JSON Web Token 彻底搞懂 JWT 登录授权

Carlos 发布于 2026-03-09 127 次阅读


在传统的 Web 开发中,我们通常使用 Session 来保持用户的登录状态。但在如今前后端分离、微服务盛行的时代(比如黑马商城这种架构),基于 Token 的无状态登录成为了主流,而 JWT(JSON Web Token) 则是其中的绝对霸主。

今天,我们就结合标准的登录时序图,从底层逻辑到 Java 代码实现,一次性把 JWT 登录彻底打通。

一、 登录核心逻辑拆解(结合时序图)

根据黑马商城的登录业务时序图,一次完整的登录流程可以拆分为以下 6 个关键步骤:

  1. 用户输入与提交 (Step 1-2):用户在浏览器前端页面输入账号和密码,点击登录,前端将包含凭证的表单以 POST 请求发给黑马商城的服务端。
  2. 查询用户信息 (Step 3.1-3.2):服务端接收到请求后,提取出用户名,去数据库(MySQL)中根据用户名查询对应的用户记录。
  3. 校验用户信息 (Step 3.3)
    • 如果数据库没查到该用户,说明账号不存在,直接返回登录失败。
    • 如果查到了用户,则将数据库中存储的密码(通常是加密后的密文)与用户提交的密码进行比对校验。密码错误则返回失败。
  4. 生成 JWT (Step 4)这是最核心的一步! 密码校验通过后,代表登录成功。服务端会根据该用户的唯一标识(如 UserID)生成一串加密的字符串,这就是 JWT。
  5. 返回结果 (Step 5):服务端将生成的 JWT Token 以及用户的基本信息封装成统一的响应格式(VO),返回给前端浏览器。
  6. 前端保存与后续请求 (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 的工具包,最常用的是 jjwthutool-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,就能立刻解析出用户的身份。

✨职务:华夏大地区域代理人 | 熬夜秃头项目主理人 💳黑卡:校园一卡通全球辅导版持有者 📍地点:宇宙-银河系-地球-东北蹲分部 🥂生活方式:沉迷于廉价多巴胺 | 致力于在该醒的时候睡觉 🚫拒绝:拒绝早起 | 拒绝内卷| 拒绝借钱 简介:虽然我没钱,但我有时间;虽然我没才华,但我有脾气。
最后更新于 2026-03-09