跳到主要内容

JWT 认证

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 是目前最流行的无状态认证机制,广泛应用于分布式系统、微服务架构和单页应用(SPA)。

为什么需要 JWT?

在传统的 Session-Cookie 认证中,服务器需要存储每个用户的会话状态。这在分布式系统中带来了挑战:

  1. 会话共享问题 - 多台服务器之间需要同步会话状态
  2. 服务器负担 - 需要维护大量的会话数据
  3. 跨域限制 - Cookie 受同源策略限制

JWT 通过将用户状态编码在令牌本身中,实现了无状态认证:

┌─────────┐                                    ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│ 1. 登录请求 (username, password) │
│ ───────────────────────────────────────────> │
│ │
│ 2. 验证成功,生成 JWT(包含用户信息和签名) │
│ │
│ 3. 返回 JWT │
│ <─────────────────────────────────────────── │
│ │
│ 4. 后续请求携带 JWT (Authorization Header) │
│ ───────────────────────────────────────────> │
│ 5. 验证签名和过期时间,无需查询数据库 │
│ 6. 返回受保护资源 │
│ <─────────────────────────────────────────── │

JWT 结构

JWT 由三部分组成,用点号(.)分隔:

xxxxx.yyyyy.zzzzz
↑ ↑ ↑
Header Payload Signature

Header(头部)

Header 包含令牌类型和签名算法:

{
"alg": "HS256",
"typ": "JWT"
}
  • alg: 签名算法(HS256、HS512、RS256、ES256 等)
  • typ: 令牌类型,通常为 JWT

Base64Url 编码后:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload(载荷)

Payload 包含声明(claims),即要传输的数据:

{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1516242622
}

Registered Claims(注册声明) - 预定义的、建议使用的声明:

声明含义说明
issIssuer签发者
subSubject主题(用户标识)
audAudience接收方
expExpiration Time过期时间(Unix 时间戳)
nbfNot Before生效时间
iatIssued At签发时间
jtiJWT ID唯一标识

Public Claims(公共声明) - 自定义的、避免冲突的声明

Private Claims(私有声明) - 应用内部使用的自定义声明

Signature(签名)

签名用于验证消息未被篡改,并验证发送者身份:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)

签名算法的选择:

算法类型说明
HS256对称加密HMAC + SHA-256,密钥共享
HS512对称加密HMAC + SHA-512,密钥共享
RS256非对称加密RSA + SHA-256,私钥签名,公钥验证
ES256非对称加密ECDSA + SHA-256,椭圆曲线

JWT 完整实现

Java 实现(Spring Boot + JJWT)

/**
* JWT 配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
// 密钥(对称加密使用)
private String secret;

// 私钥路径(非对称加密使用)
private String privateKeyPath;

// 公钥路径(非对称加密使用)
private String publicKeyPath;

// 访问令牌过期时间(毫秒)
private long accessTokenExpiration = 3600000; // 1小时

// 刷新令牌过期时间(毫秒)
private long refreshTokenExpiration = 604800000; // 7天

// 签发者
private String issuer = "myapp";
}

/**
* JWT 工具类
*/
@Component
public class JwtUtil {

@Autowired
private JwtProperties jwtProperties;

private SecretKey secretKey;

@PostConstruct
public void init() {
// 使用 HS256 算法生成密钥
if (jwtProperties.getSecret() != null) {
this.secretKey = Keys.hmacShaKeyFor(
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
);
} else {
// 自动生成安全密钥
this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
}
}

/**
* 生成访问令牌
*/
public String generateAccessToken(UserDetails user) {
Date now = new Date();
Date expiration = new Date(
now.getTime() + jwtProperties.getAccessTokenExpiration()
);

return Jwts.builder()
// Header
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// Payload
.setSubject(user.getUsername())
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiration)
.setId(UUID.randomUUID().toString())
// 自定义声明
.claim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.claim("type", "access")
// 签名
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

/**
* 生成刷新令牌
*/
public String generateRefreshToken(UserDetails user) {
Date now = new Date();
Date expiration = new Date(
now.getTime() + jwtProperties.getRefreshTokenExpiration()
);

return Jwts.builder()
.setSubject(user.getUsername())
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiration)
.setId(UUID.randomUUID().toString())
.claim("type", "refresh")
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

/**
* 验证并解析令牌
*/
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new JwtException("令牌已过期");
} catch (UnsupportedJwtException e) {
throw new JwtException("不支持的令牌格式");
} catch (MalformedJwtException e) {
throw new JwtException("令牌格式错误");
} catch (SignatureException e) {
throw new JwtException("签名验证失败");
} catch (IllegalArgumentException e) {
throw new JwtException("令牌为空或非法");
}
}

/**
* 验证令牌是否有效
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException e) {
return false;
}
}

/**
* 检查令牌是否过期
*/
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}

/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}

/**
* 从令牌中获取角色
*/
@SuppressWarnings("unchecked")
public List<String> getRolesFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("roles", List.class);
}

/**
* 获取令牌剩余有效时间(毫秒)
*/
public long getExpirationTime(String token) {
Claims claims = parseToken(token);
return claims.getExpiration().getTime() - System.currentTimeMillis();
}
}

Spring Security 集成

/**
* JWT 认证过滤器
* 从请求中提取并验证 JWT
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtUtil jwtUtil;

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

// 1. 从请求头中获取令牌
String authHeader = request.getHeader("Authorization");

// 2. 检查是否为 Bearer 令牌
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

String token = authHeader.substring(7);

try {
// 3. 验证令牌
Claims claims = jwtUtil.parseToken(token);

// 4. 检查令牌类型
String tokenType = claims.get("type", String.class);
if (!"access".equals(tokenType)) {
throw new JwtException("无效的令牌类型");
}

// 5. 获取用户信息
String username = claims.getSubject();

// 6. 检查是否已在 SecurityContext 中
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);

// 7. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

authentication.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request)
);

// 8. 设置 SecurityContext
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}

} catch (JwtException e) {
// 令牌验证失败,返回 401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("无效的令牌: " + e.getMessage());
return;
}

filterChain.doFilter(request, response);
}
}

/**
* Spring Security 配置
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(JWT 是无状态的,不需要 CSRF 防护)
.csrf(csrf -> csrf.disable())

// 配置无状态会话
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

// 配置异常处理
.exceptionHandling(exception ->
exception.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)

// 配置授权规则
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

/**
* 认证入口点
* 处理未认证请求
*/
@Component
public class JwtAuthenticationEntryPoint
implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {

response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

Map<String, Object> error = new HashMap<>();
error.put("code", 401);
error.put("message", "未提供有效的认证令牌");
error.put("path", request.getRequestURI());

ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}

认证控制器

/**
* 认证控制器
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtUtil jwtUtil;

@Autowired
private UserService userService;

@Autowired
private RefreshTokenService refreshTokenService;

/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody @Valid LoginRequest request) {
try {
// 1. 执行认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);

// 2. 获取用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();

// 3. 生成令牌
String accessToken = jwtUtil.generateAccessToken(userDetails);
String refreshToken = jwtUtil.generateRefreshToken(userDetails);

// 4. 保存刷新令牌到数据库
refreshTokenService.saveRefreshToken(
userDetails.getUsername(),
refreshToken
);

// 5. 返回令牌
return ResponseEntity.ok(new AuthResponse(
accessToken,
refreshToken,
jwtUtil.getExpirationTime(accessToken),
"Bearer"
));

} catch (BadCredentialsException e) {
return ResponseEntity.status(401)
.body("用户名或密码错误");
}
}

/**
* 刷新令牌
*/
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(
@RequestBody RefreshTokenRequest request) {

String refreshToken = request.getRefreshToken();

try {
// 1. 验证刷新令牌
Claims claims = jwtUtil.parseToken(refreshToken);

// 2. 检查令牌类型
if (!"refresh".equals(claims.get("type"))) {
return ResponseEntity.status(401)
.body("无效的刷新令牌");
}

// 3. 检查数据库中是否存在
String username = claims.getSubject();
if (!refreshTokenService.validateRefreshToken(username, refreshToken)) {
return ResponseEntity.status(401)
.body("刷新令牌已被撤销");
}

// 4. 生成新的访问令牌
UserDetails userDetails = userService.loadUserByUsername(username);
String newAccessToken = jwtUtil.generateAccessToken(userDetails);

return ResponseEntity.ok(new AuthResponse(
newAccessToken,
refreshToken,
jwtUtil.getExpirationTime(newAccessToken),
"Bearer"
));

} catch (JwtException e) {
return ResponseEntity.status(401)
.body("刷新令牌已过期或无效");
}
}

/**
* 用户登出
*/
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader("Authorization") String authHeader) {
// 1. 提取令牌
String token = authHeader.substring(7);

// 2. 将令牌加入黑名单
jwtUtil.blacklistToken(token);

// 3. 撤销刷新令牌
String username = jwtUtil.getUsernameFromToken(token);
refreshTokenService.revokeRefreshToken(username);

return ResponseEntity.ok("登出成功");
}
}

前端使用示例

/**
* JWT 认证服务
*/
class AuthService {
constructor() {
this.accessToken = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
this.refreshPromise = null;
}

/**
* 登录
*/
async login(username, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});

if (!response.ok) {
throw new Error('登录失败');
}

const data = await response.json();
this.setTokens(data.accessToken, data.refreshToken);

return data;
}

/**
* 设置令牌
*/
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}

/**
* 获取访问令牌
*/
getAccessToken() {
return this.accessToken;
}

/**
* 刷新访问令牌
*/
async refreshAccessToken() {
// 防止重复刷新
if (this.refreshPromise) {
return this.refreshPromise;
}

this.refreshPromise = fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
})
.then(response => {
if (!response.ok) {
throw new Error('刷新令牌失败');
}
return response.json();
})
.then(data => {
this.setTokens(data.accessToken, data.refreshToken);
return data.accessToken;
})
.finally(() => {
this.refreshPromise = null;
});

return this.refreshPromise;
}

/**
* 登出
*/
async logout() {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});

this.clearTokens();
}

/**
* 清除令牌
*/
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}

/**
* 带自动刷新的 HTTP 客户端
*/
class HttpClient {
constructor(authService) {
this.authService = authService;
}

async request(url, options = {}) {
// 添加认证头
const token = this.authService.getAccessToken();
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}

let response = await fetch(url, options);

// 令牌过期,尝试刷新
if (response.status === 401) {
try {
const newToken = await this.authService.refreshAccessToken();
options.headers['Authorization'] = `Bearer ${newToken}`;
response = await fetch(url, options);
} catch (error) {
// 刷新失败,跳转到登录页
this.authService.clearTokens();
window.location.href = '/login';
throw error;
}
}

return response;
}

get(url) {
return this.request(url, { method: 'GET' });
}

post(url, data) {
return this.request(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}

// 使用示例
const authService = new AuthService();
const httpClient = new HttpClient(authService);

// 登录
await authService.login('user', 'password');

// 访问受保护资源
const response = await httpClient.get('/api/protected/data');
const data = await response.json();

JWT 安全最佳实践

密钥管理

/**
* 密钥管理服务
*/
@Service
public class KeyManagementService {

/**
* 生成安全的随机密钥
*/
public String generateSecureKey() {
// 生成 256 位(32 字节)的随机密钥
byte[] keyBytes = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(keyBytes);
return Base64.getEncoder().encodeToString(keyBytes);
}

/**
* 生成 RSA 密钥对
*/
public KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
return keyGen.generateKeyPair();
}

/**
* 从环境变量读取密钥
*/
public SecretKey loadKeyFromEnvironment() {
String secret = System.getenv("JWT_SECRET");
if (secret == null || secret.length() < 32) {
throw new IllegalStateException("JWT_SECRET 环境变量未设置或长度不足");
}
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
}

令牌黑名单

/**
* 令牌黑名单服务
* 用于处理令牌提前失效(如用户登出)
*/
@Service
public class TokenBlacklistService {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String BLACKLIST_PREFIX = "jwt:blacklist:";

/**
* 将令牌加入黑名单
*/
public void blacklistToken(String token) {
// 解析令牌获取过期时间
Claims claims = jwtUtil.parseToken(token);
long expirationTime = claims.getExpiration().getTime();
long currentTime = System.currentTimeMillis();
long ttl = expirationTime - currentTime;

if (ttl > 0) {
String key = BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1", ttl, TimeUnit.MILLISECONDS);
}
}

/**
* 检查令牌是否在黑名单中
*/
public boolean isBlacklisted(String token) {
String key = BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}

常见安全错误

// ❌ 错误 1: 使用弱密钥
String secret = "my-secret-key"; // 太短,容易被暴力破解

// ✅ 正确: 使用强密钥
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);


// ❌ 错误 2: 在 Payload 中存储敏感信息
Jwts.builder()
.claim("password", user.getPassword()) // 危险!
.claim("ssn", user.getSocialSecurityNumber()) // 危险!
.signWith(key)
.compact();

// ✅ 正确: 只存储必要信息,敏感信息存储在服务器
Jwts.builder()
.setSubject(user.getId())
.claim("role", user.getRole())
.signWith(key)
.compact();


// ❌ 错误 3: 没有设置过期时间
Jwts.builder()
.setSubject(user.getUsername())
.signWith(key)
.compact(); // 永不过期!

// ✅ 正确: 设置合理的过期时间
Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时
.signWith(key)
.compact();


// ❌ 错误 4: 在前端存储令牌时未考虑 XSS
localStorage.setItem('token', token); // 容易受到 XSS 攻击

// ✅ 正确: 使用 HttpOnly Cookie(推荐)或内存存储
document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict`;


// ❌ 错误 5: 不验证签名算法
Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token); // 可能被算法切换攻击

// ✅ 正确: 明确指定允许的算法
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);

JWT 与 Session 对比

特性JWTSession
存储位置客户端服务器端
服务器状态无状态有状态
扩展性易于水平扩展需要共享会话存储
性能无需查询数据库需要查询会话存储
安全性令牌一旦颁发无法撤销可随时使会话失效
跨域支持天然支持需要额外配置
令牌大小较大(包含用户信息)很小(仅 Session ID)
适用场景分布式系统、移动应用传统 Web 应用

小结

JWT 是一种强大的无状态认证机制,适用于现代分布式系统:

  1. 核心优势

    • 无状态,易于水平扩展
    • 自包含,减少数据库查询
    • 跨域友好,适合微服务架构
  2. 安全要点

    • 使用强密钥和安全的签名算法
    • 设置合理的过期时间
    • 不在 Payload 中存储敏感信息
    • 实现令牌刷新和黑名单机制
  3. 最佳实践

    • 访问令牌短期有效(15-60 分钟)
    • 刷新令牌长期有效(7-30 天)
    • 使用 HTTPS 传输所有令牌
    • 考虑使用 HttpOnly Cookie 存储令牌
  4. 注意事项

    • JWT 不是加密的,只是编码和签名
    • 令牌大小会随着声明增加而增加
    • 无法实现服务端主动失效(需要黑名单)
    • 需要额外的机制处理令牌刷新