JWT(JSON Web Token)详解:从原理到实战,彻底搞懂身份认证

标签:JWT 身份认证 Spring Boot 后端开发


目录

  1. JWT 是什么,解决了什么问题
  2. Session 认证的痛点
  3. JWT 的结构详解
  4. 完整认证流程
  5. 为什么天然支持水平扩展
  6. Token 里该存什么,不该存什么
  7. JWT 的缺点与解决方案
  8. Spring Boot 实战代码

一、JWT 是什么,解决了什么问题

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以紧凑、自包含的方式安全传递信息。它最核心的使用场景是身份认证

简单来说:用户登录后,服务端生成一个 Token 返回给客户端。之后每次请求,客户端带上这个 Token,服务端验证通过即可识别用户身份,全程不需要在服务端存储任何会话状态


二、Session 认证的痛点

传统 Session 认证流程:用户登录 → 服务端创建 Session 存储在内存中 → 返回 SessionId 给客户端(Cookie)→ 之后每次请求带上 SessionId,服务端查询 Session 验证身份。

问题在于,Session 状态存储在某一台服务器的内存中。一旦系统水平扩展到多台服务器,下次请求被负载均衡打到另一台机器,那台服务器找不到 Session,直接认为未登录。

用户登录 ──▶ 服务器 A(Session 存这里 ✅)
下次请求 ──▶ 服务器 B(没有 Session ❌ 认为未登录)
下次请求 ──▶ 服务器 C(没有 Session ❌ 认为未登录)

解法要么用 Sticky Session(限制扩展灵活性),要么上 Redis 共享 Session(引入额外依赖和单点风险)。JWT 从根本上规避了这个问题。


三、JWT 的结构详解

JWT 由三部分组成,用 . 分隔:

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMDA4NiJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Header(头部)

{
  "alg": "HS256",
  "typ": "JWT"
}

Base64URL 编码后作为 Token 的第一段。

Payload(载荷)

{
  "sub": "10086",
  "role": "user",
  "iat": 1700000000,
  "exp": 1700003600
}

⚠️ 注意:Payload 只是 Base64URL 编码,不是加密!任何人都可以解码读取内容。绝对不能存密码、手机号等敏感信息。

Signature(签名)

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret_key    // 只有服务端知道
)

签名是防篡改的关键。即使有人修改了 Payload,重新计算的签名也会与原签名不符,服务端验签失败,请求被拒绝。


四、完整认证流程

1. 客户端  POST /login { username, password }
2. 服务端  验证密码 → 生成 JWT,用 secret 签名 → 返回 Token
3. 客户端  将 Token 存储(localStorage 或 Cookie)
4. 客户端  后续请求头携带:Authorization: Bearer <token>
5. 服务端  验签 → 检查 exp → 读取 userId → 处理业务逻辑

五、为什么天然支持水平扩展

验证一个 JWT 所需的全部信息都在 Token 本身里,服务端只需持有同一个 secret_key,就可以完成验签。这是一个纯计算过程,不依赖任何外部存储

服务器 A 收到 JWT → 用 secret 验签 → 通过,user=10086
服务器 B 收到 JWT → 用 secret 验签 → 通过,user=10086
服务器 C 收到 JWT → 用 secret 验签 → 通过,user=10086

新加一台服务器,只要配置好同一个 secret,立刻就能正确处理所有请求,不需要做任何数据同步。这就是"无状态"的本质。


六、Token 里该存什么,不该存什么

字段 是否适合放入 Token 原因
userId ✅ 适合 永远不变的唯一标识
role ⚠️ 谨慎 变化不频繁可以接受,但有延迟风险
用户名 ❌ 不适合 可能会变,Token 无法同步更新
头像 / 邮箱 ❌ 不适合 同上,且增大 Token 体积
密码 / 手机号 ❌ 绝对不行 Payload 未加密,明文可读

正确做法:Token 只存 userId,所有会变的业务数据每次从数据库取最新值。Token 的职责只是证明你是谁,不是携带用户档案。


七、JWT 的缺点与解决方案

缺点 说明 解决方案
无法主动吊销 退出登录后 Token 在过期前仍有效 Redis 黑名单,登出时写入
权限变更延迟 role 更新后旧 Token 仍是旧权限 缩短有效期(15分钟)+ Refresh Token
secret 泄露风险 任何人都能伪造合法 Token 定期轮换 secret,使用 RS256 非对称加密
Token 体积偏大 每次请求都要携带 Payload 只放必要字段

八、Spring Boot 实战代码

引入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

JwtUtil 核心工具类

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    private static final long EXPIRATION_MS = 1000 * 60 * 60; // 1小时

    // 生成 Token,只存 userId 和 role
    public String generateToken(String userId, String role) {
        return Jwts.builder()
                .setSubject(userId)
                .claim("role", role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
                .signWith(getSigningKey())
                .compact();
    }

    // 验证并解析 Token
    public Claims parseToken(String token) {
        // 签名不对或已过期会直接抛出异常
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }
}

拦截器验证 Token

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse res, Object handler) {
        String header = req.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            res.setStatus(401);
            return false;
        }
        try {
            Claims claims = jwtUtil.parseToken(header.substring(7));
            req.setAttribute("userId", claims.getSubject());
            req.setAttribute("role", claims.get("role"));
            return true;
        } catch (JwtException e) {
            res.setStatus(401); // 签名错误或已过期
            return false;
        }
    }
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")       // 拦截所有 /api 接口
                .excludePathPatterns("/login");   // 放行登录接口
    }
}

💡 建议:将 jwt.secret 配置到环境变量或 Nacos/Apollo 配置中心,不要硬编码在代码里。生产环境推荐使用 RS256(非对称加密),即使 secret 泄露影响也更可控。


如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论 👇

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐