JWT(JSON Web Token)详解:从原理到实战,彻底搞懂身份认证
JWT(JSON Web Token)详解:从原理到实战,彻底搞懂身份认证
标签:
JWT身份认证Spring Boot后端开发
目录
- JWT 是什么,解决了什么问题
- Session 认证的痛点
- JWT 的结构详解
- 完整认证流程
- 为什么天然支持水平扩展
- Token 里该存什么,不该存什么
- JWT 的缺点与解决方案
- 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 泄露影响也更可控。
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论 👇
更多推荐



所有评论(0)