在传统的 Web 开发中,我们通常使用 Session 来保持用户的登录状态。但在如今前后端分离、微服务盛行的时代(比如黑马商城这种架构),基于 Token 的无状态登录成为了主流,而 JWT(JSON Web Token) 则是其中的绝对霸主。
今天,我们就结合标准的登录时序图,从底层逻辑到 Java 代码实现,一次性把 JWT 登录彻底打通。
一、 登录核心逻辑拆解(结合时序图)
根据黑马商城的登录业务时序图,一次完整的登录流程可以拆分为以下 6 个关键步骤:
- 用户输入与提交 (Step 1-2):用户在浏览器前端页面输入账号和密码,点击登录,前端将包含凭证的表单以 POST 请求发给黑马商城的服务端。
- 查询用户信息 (Step 3.1-3.2):服务端接收到请求后,提取出用户名,去数据库(MySQL)中根据用户名查询对应的用户记录。
- 校验用户信息 (Step 3.3):
- 如果数据库没查到该用户,说明账号不存在,直接返回登录失败。
- 如果查到了用户,则将数据库中存储的密码(通常是加密后的密文)与用户提交的密码进行比对校验。密码错误则返回失败。
- 生成 JWT (Step 4):这是最核心的一步! 密码校验通过后,代表登录成功。服务端会根据该用户的唯一标识(如 UserID)生成一串加密的字符串,这就是 JWT。
- 返回结果 (Step 5):服务端将生成的 JWT Token 以及用户的基本信息封装成统一的响应格式(VO),返回给前端浏览器。
- 前端保存与后续请求 (Step 6):浏览器提示登录成功,并将接收到的 JWT 存储在本地(如 LocalStorage)。在后续的所有请求中,前端都会在 HTTP 请求头(Header)中带上这个 Token,服务端只需校验 Token 的有效性,即可认出“你是谁”。
二、 JWT 到底长什么样?
JWT 本质上是一个包含用户信息的加密字符串,由三部分组成,中间用 . 隔开:Header.Payload.Signature
- Header(头部):记录令牌类型和签名算法。
- Payload(载荷):存放有效信息,比如用户的 ID、角色等(千万不要在这里放密码等敏感信息,因为它是可以被 base64 解码看懂的!)。
- Signature(签名):防止 Token 被篡改。服务端用只有自己知道的“秘钥(Secret Key)”,对前两部分进行签名。如果黑客篡改了 Payload,签名就会失效,服务端一眼就能识破。
三、 Spring Boot 实战代码实现
在黑马商城项目中,我们通常结合 MyBatis-Plus 来完成数据库操作。以下是严格对应时序图逻辑的 Java 代码实现。
1. 引入 JWT 依赖 (pom.xml)
首先,确保项目中引入了操作 JWT 的工具包,最常用的是 jjwt 或 hutool-jwt,这里以基础工具为例:
XML
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 编写 JWT 工具类 (JwtTool.java)
我们需要一个工具类来负责生成和解析 Token。
Java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTool {
// 签名秘钥,实际项目中应配置在 application.yml 中
private static final String SECRET_KEY = "HeimaMallSecretKey!@#";
// Token 有效期:这里设置为 2 小时
private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000;
/**
* 生成 JWT (对应时序图 Step 4)
* @param userId 用户的数据库主键 ID
* @return 签发好的 JWT 字符串
*/
public String createToken(Long userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
return Jwts.builder()
.setClaims(claims) // 设置载荷 Payload
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) // 过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法和秘钥
.compact();
}
}
3. 控制层 UserController.java
接收前端发送的包含账号密码的 DTO 对象。
Java
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("/login")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO loginDTO) {
// 调用 Service 层执行具体的登录逻辑
UserLoginVO vo = userService.login(loginDTO);
return Result.success(vo);
}
}
4. 业务逻辑层 UserServiceImpl.java (核心)
这一步完美映射了时序图中的 3.1 到 5 的所有步骤。
Java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private JwtTool jwtTool;
@Autowired
private PasswordEncoder passwordEncoder; // 假设使用 BCrypt 密码编码器
@Override
public UserLoginVO login(UserLoginDTO loginDTO) {
// Step 3.1 & 3.2: 根据用户名查询数据库
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, loginDTO.getUsername());
User user = this.getOne(wrapper);
// Step 3.3: 校验用户信息
// 1. 账号不存在
if (user == null) {
throw new CustomException("用户名或密码错误");
}
// 2. 密码比对校验 (明文密码 vs 数据库里的密文密码)
boolean isMatch = passwordEncoder.matches(loginDTO.getPassword(), user.getPassword());
if (!isMatch) {
throw new CustomException("用户名或密码错误");
}
// 3. 检查账号状态 (比如是否被拉黑封禁)
if (user.getStatus() == 0) {
throw new CustomException("该账号已被冻结,请联系管理员");
}
// Step 4: 登录成功,生成 JWT
String token = jwtTool.createToken(user.getId());
// Step 5: 封装返回结果并返回给 Controller
UserLoginVO vo = new UserLoginVO();
vo.setUserId(user.getId());
vo.setUsername(user.getUsername());
vo.setToken(token); // 将生成的 JWT 塞给前端
return vo;
}
}
四、 总结
通过上述流程,我们成功实现了一个健壮的无状态登录接口。相比于传统的 Session,JWT 最大的优势在于服务器不需要占用内存去存储用户的登录状态,所有的身份信息都在那串 Token 里。这使得我们的服务非常容易做集群水平扩展——无论用户的下一次请求打到了哪台服务器上,只要该服务器拥有相同的 Secret Key,就能立刻解析出用户的身份。
Comments NOTHING