JWT 认证全面解析:原理、流程与 Spring Boot 实战
一、什么是 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象安全地传递信息。这些信息可以被验证和信任,因为它是数字签名的。
核心特征:
- 无状态:服务端不需要存储 Session,Token 本身包含所有信息
- 自包含:Token 中携带用户身份和权限数据
- 可验证:通过签名确保 Token 未被篡改
二、JWT 的结构
一个 JWT 由三部分组成,用 . 分隔:
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIyMDExMDEwMSIsIm5hbWUiOiLlvKDkuIkifQ.signature
|______ Header ______||_________________ Payload __________________||_ Signature _|
2.1 Header(头部)
声明 Token 类型和签名算法:
json
{
"alg": "RS256", // 签名算法
"typ": "JWT" // Token 类型
}
经过 Base64Url 编码后成为第一段。
2.2 Payload(载荷)
携带的实际数据(Claims):
json
{
"sub": "20110101", // Subject:用户标识
"name": "张三", // 自定义字段
"role": "admin", // 自定义字段
"iat": 1718150400, // Issued At:签发时间
"exp": 1718236800, // Expiration:过期时间
"aud": "my-service" // Audience:接收方
}
标准 Claims(预定义):
| 字段 | 全称 | 说明 |
|---|---|---|
| iss | Issuer | 签发人 |
| sub | Subject | 主题(通常是用户ID) |
| aud | Audience | 受众(Token 面向的服务) |
| exp | Expiration | 过期时间(Unix 时间戳) |
| nbf | Not Before | 生效时间 |
| iat | Issued At | 签发时间 |
| jti | JWT ID | 唯一标识(用于防重放) |
经过 Base64Url 编码后成为第二段。
注意 :Payload 只是 Base64 编码,不是加密。任何人都能解码看到内容,所以不要放敏感信息(如密码)。
2.3 Signature(签名)
用于验证 Token 未被篡改:
Signature = Algorithm(
Base64Url(Header) + "." + Base64Url(Payload),
密钥
)
- 对称算法(HS256):用同一个 Secret 签名和验证
- 非对称算法(RS256):用私钥签名,公钥验证
三、签名算法对比
3.1 对称加密(HMAC)
签发方:HMAC-SHA256(header.payload, secret) → signature
验证方:HMAC-SHA256(header.payload, secret) → 比较 signature
| 优点 | 缺点 |
|---|---|
| 简单,性能高 | 签发方和验证方必须共享同一个 secret |
| 适合单服务 | secret 泄露则任何人可以伪造 Token |
3.2 非对称加密(RSA)
签发方(认证中心):RSA-SHA256(header.payload, 私钥) → signature
验证方(业务服务):RSA-SHA256-Verify(header.payload, signature, 公钥) → true/false
| 优点 | 缺点 |
|---|---|
| 只有认证中心持有私钥,业务服务只需公钥 | 性能略低于 HMAC |
| 公钥可以公开分发,不怕泄露 | 密钥管理复杂一些 |
| 适合微服务架构 | - |
示例:项目使用 RS256(RSA + SHA256),application.yml 中配置了公钥用于验证:
yaml
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: |
-----BEGIN PUBLIC KEY-----
xxxxxxxx...
-----END PUBLIC KEY-----
四、认证流程
4.1 完整时序
用户/前端 认证中心(Auth Server) 业务服务(Resource Server)
│ │ │
│── 1.登录(用户名+密码) ──→ │ │
│ │── 2.验证账号密码 │
│ │── 3.生成JWT(用私钥签名) │
│← 4.返回 JWT Token ── │ │
│ │ │
│── 5.请求业务接口 ──→ │ │
│ Header: Authorization: │ │
│ Bearer eyJhbGciOi... │ │
│ │ │── 6.取出Token
│ │ │── 7.用公钥验签
│ │ │── 8.检查是否过期
│ │ │── 9.解析用户信息
│ │ │── 10.执行业务逻辑
│← 11.返回业务数据 ── │ │
4.2 各步骤详解
| 步骤 | 说明 |
|---|---|
| 1-4 | 认证阶段:只发生一次(或 Token 过期后重新认证) |
| 5 | 前端每次请求在 Header 中携带 Token |
| 6 | Security Filter 从请求头提取 Token |
| 7 | 用公钥验证签名(确保 Token 未被篡改) |
| 8 | 检查 exp 字段(确保 Token 未过期) |
| 9 | 从 Payload 中解析用户ID、角色等信息 |
| 10-11 | 正常执行业务逻辑并返回 |
4.3 Token 存储位置(前端)
| 存储位置 | 优点 | 缺点 |
|---|---|---|
| localStorage | 持久化,刷新页面不丢失 | 容易被 XSS 攻击读取 |
| sessionStorage | 关闭标签页后清除 | 同样有 XSS 风险 |
| Cookie (httpOnly) | XSS 无法读取 | 需要处理 CSRF |
| 内存变量 | 最安全 | 刷新页面丢失 |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
五、Spring Security 中的 JWT 处理流程
5.1 Filter 链
HTTP请求
↓
SecurityFilterChain
├── CharacterEncodingFilter
├── CorsFilter
├── BearerTokenAuthenticationFilter ← JWT 在这里被处理
│ ├── 从 Authorization Header 提取 Token
│ ├── 调用 JwtDecoder 解码验证
│ ├── 验证通过 → 创建 Authentication 对象放入 SecurityContext
│ └── 验证失败 → 返回 401
├── AuthorizationFilter
│ ├── 检查 SecurityContext 中是否有认证信息
│ ├── 检查是否匹配 permitAll 规则
│ └── 不匹配且未认证 → 返回 403
└── DispatcherServlet → Controller
5.2 permitAll 的位置
yaml
security:
matchers:
- path: /api/xxxx/**
attribute: permitAll
这条规则在 AuthorizationFilter 中生效:
- 请求路径匹配
/api/xxxx/**→ 跳过认证检查,直接放行 - 请求路径不在 permitAll 列表中 → 必须有有效的 JWT Token
5.3 JwtDecoder 的工作
java
// Spring Security 内部逻辑(简化)
public Jwt decode(String token) {
// 1. 按 "." 分割为三段
String[] parts = token.split("\\.");
// 2. Base64 解码 Header 和 Payload
Header header = decodeHeader(parts[0]);
Claims claims = decodeClaims(parts[1]);
// 3. 用公钥验证签名
boolean valid = rsaVerify(parts[0] + "." + parts[1], parts[2], publicKey);
if (!valid) throw new InvalidTokenException("签名验证失败");
// 4. 检查过期时间
if (claims.getExp().before(new Date())) {
throw new ExpiredTokenException("Token已过期");
}
// 5. 检查 audience
if (!claims.getAud().contains(expectedAudience)) {
throw new InvalidTokenException("Token受众不匹配");
}
return new Jwt(header, claims);
}
六、Token 刷新机制
6.1 为什么需要刷新
- Token 有过期时间(如2小时),过期后用户需要重新登录
- 频繁让用户重新登录体验差
- 用 Refresh Token 机制实现"无感续期"
6.2 双 Token 机制
| Token 类型 | 有效期 | 用途 | 存储 |
|---|---|---|---|
| Access Token | 短(如2小时) | 访问业务接口 | 内存/localStorage |
| Refresh Token | 长(如7天) | 换取新的 Access Token | httpOnly Cookie |
6.3 刷新流程
前端 认证中心
│ │
│── 请求业务接口(Access Token) ──→ │
│← 401 Token过期 ── │
│ │
│── 刷新请求(Refresh Token) ──→ │
│ │── 验证Refresh Token
│ │── 生成新Access Token
│← 返回新Access Token ── │
│ │
│── 重试业务请求(新Token) ──→ │
│← 正常返回 ── │
七、JWT vs Session 对比
| 维度 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端(内存/Redis) |
| 服务端状态 | 无状态 | 有状态 |
| 水平扩展 | 天然支持(任何节点都能验证) | 需要 Session 共享(如 Redis) |
| 性能 | 每次请求解析 Token(CPU) | 每次请求查 Session(IO) |
| 注销 | 困难(Token 签发后无法撤回) | 简单(删除 Session 即可) |
| 信息传递 | Token 自带用户信息 | 需要通过 SessionId 查服务端 |
| 安全性 | 泄露后无法立即失效 | 可以立即失效 |
| 跨域 | 天然支持 | 需要额外处理 |
| 适用场景 | 微服务、移动端、第三方API | 传统单体Web应用 |
八、安全注意事项
| 风险 | 防护措施 |
|---|---|
| Token 泄露 | 短过期时间 + HTTPS + httpOnly Cookie |
| Token 篡改 | 签名验证(RS256/HS256) |
| 重放攻击 | jti 唯一标识 + 短过期时间 |
| XSS 窃取 | httpOnly Cookie 存储 Token |
| 暴力破解 | RS256 用 2048+ 位密钥 |
| 无法注销 | 维护 Token 黑名单(Redis) |
| Payload 信息泄露 | 不放敏感数据,只放 ID 和角色 |
九、完整代码示例
9.1 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
9.2 配置
yaml
app:
jwt:
secret: xxxxxxxxxxxx!!
expiration: 7200 # Access Token 有效期(秒)
refresh-expiration: 604800 # Refresh Token 有效期(秒)
9.3 JWT 工具类
java
package com.example.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* JWT 工具类:生成、解析、验证 Token.
*/
@Component
public class JwtUtil {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成 Token.
*/
public String generateToken(String userId, String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
return Jwts.builder()
.claims(claims)
.subject(userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey())
.compact();
}
/**
* 解析 Token,返回 Claims.
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 验证 Token 是否有效.
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
return false; // 过期
} catch (Exception e) {
return false; // 无效
}
}
/**
* 从 Token 中获取用户ID.
*/
public String getUserId(String token) {
return parseToken(token).getSubject();
}
/**
* 从 Token 中获取用户名.
*/
public String getUsername(String token) {
return parseToken(token).get("username", String.class);
}
/**
* 判断 Token 是否过期.
*/
public boolean isTokenExpired(String token) {
try {
Date exp = parseToken(token).getExpiration();
return exp.before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
9.4 JWT 认证过滤器
java
package com.example.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* JWT 认证过滤器.
* 每次请求检查 Authorization Header 中的 Token.
*/
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTH_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 1. 从请求头提取 Token
String header = request.getHeader(AUTH_HEADER);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
String token = header.substring(TOKEN_PREFIX.length());
// 2. 验证 Token
if (!jwtUtil.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Token无效或已过期\"}");
return;
}
// 3. 解析用户信息,放入 SecurityContext
String userId = jwtUtil.getUserId(token);
String username = jwtUtil.getUsername(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, null);
// 可以把更多信息放到 details 中
authentication.setDetails(username);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4. 继续执行后续 Filter 和 Controller
chain.doFilter(request, response);
}
}
9.5 Security 配置类
java
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 配置.
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(JWT 无状态不需要)
.csrf(csrf -> csrf.disable())
// 无状态 Session(不创建 HttpSession)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// URL 权限规则
.authorizeHttpRequests(auth -> auth
// 放行的接口
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/api/auth/refresh").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 其他所有接口需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
9.6 登录接口(签发 Token)
java
package com.example.controller;
import com.example.security.JwtUtil;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
/**
* 登录接口:验证账号密码,签发 Token.
*/
@PostMapping("/login")
public Map<String, Object> login(@RequestBody LoginRequest request) {
// 实际项目中这里查数据库验证密码
if (!"admin".equals(request.getUsername()) || !"123456".equals(request.getPassword())) {
return Map.of("success", false, "errorMsg", "用户名或密码错误");
}
// 验证通过,生成 Token
String token = jwtUtil.generateToken("10001", "admin", "ROLE_ADMIN");
return Map.of(
"success", true,
"data", Map.of(
"token", token,
"tokenType", "Bearer",
"expiresIn", 7200
)
);
}
public static class LoginRequest {
private String username;
private String password;
// getter/setter...
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
9.7 业务接口中获取当前用户
java
package com.example.controller;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
/**
* 获取当前登录用户信息.
* 必须携带有效 Token 才能访问.
*/
@GetMapping("/me")
public Map<String, Object> getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = (String) auth.getPrincipal(); // 从 Token 中解析的 userId
String username = (String) auth.getDetails(); // 从 Token 中解析的 username
return Map.of(
"success", true,
"data", Map.of(
"userId", userId,
"username", username
)
);
}
}
9.8 测试流程
bash
# 1. 登录获取 Token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 返回:
# {"success":true,"data":{"token":"eyJhbGciOiJIUzI1NiJ9.xxx.yyy","tokenType":"Bearer","expiresIn":7200}}
# 2. 携带 Token 访问受保护接口
curl http://localhost:8080/api/user/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.yyy"
# 返回:
# {"success":true,"data":{"userId":"10001","username":"admin"}}
# 3. 不带 Token 访问(被拦截)
curl http://localhost:8080/api/user/me
# 返回:401 Unauthorized
十、总结
JWT 认证的本质:
用密码学签名代替服务端存储,实现无状态的身份验证。
核心流程:
登录 → 认证中心用私钥签发Token → 前端存储Token
→ 每次请求携带Token → 业务服务用公钥验签 → 通过则放行
关键配置:
permitAll → 跳过认证,直接放行(公开接口)
authenticated → 必须携带有效Token(受保护接口)