1. 为什么现代API需要JWT保护
在移动互联网和微服务架构盛行的今天,API已成为不同系统间通信的核心纽带。传统基于Session的认证方式暴露出诸多问题:服务器需要维护会话状态、跨域资源共享(CORS)处理复杂、移动端支持不友好等。我在实际项目中最深刻的体会是:当系统需要扩展到多台服务器时,Session共享问题会让整个架构变得异常复杂。
JSON Web Token(JWT)的出现完美解决了这些痛点。它本质上是一种开放标准(RFC 7519),通过数字签名实现自包含的安全令牌。我曾在电商平台项目中对比测试过几种方案,JWT的表现尤为突出:
- 无状态特性使服务器扩展变得简单
- 跨语言支持良好(我们同时使用Java和Node.js服务)
- 移动端集成成本大幅降低
- 令牌本身可携带业务信息(如用户基础资料)
关键提示:JWT特别适合前后端分离架构、第三方API授权、分布式微服务等场景,但不适合需要即时吊销令牌的敏感系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JWT的核心组成与工作原理
2.1 令牌结构拆解
一个标准的JWT由三部分组成,通过点号(.)连接:
code复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header部分(eyJhbGci...)通常包含:
json复制{
"alg": "HS256",
"typ": "JWT"
}
Payload部分(eyJzdWIi...)是开发者最关心的,可自定义业务字段:
json复制{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622 // 重要!必须设置过期时间
}
Signature部分(SflKxw...)是前两部分通过指定算法(如HS256)和密钥生成的签名,用于验证令牌真实性。
2.2 签名算法选型建议
根据项目安全需求,常见选择有:
| 算法类型 | 示例 | 安全性 | 适用场景 |
|---|---|---|---|
| 对称加密 | HS256 | 中 | 内部系统、性能敏感场景 |
| 非对称 | RS256 | 高 | 开放平台、高安全要求 |
| 现代加密 | EdDSA | 极高 | 金融级应用 |
我在金融项目中曾因使用HS256导致安全隐患,后来切换为RS256才通过安全审计。关键教训是:密钥管理比算法本身更重要,必须使用安全的密钥存储方案(如AWS KMS或HashiCorp Vault)。
3. 实战:Spring Boot集成JWT全流程
3.1 基础环境搭建
首先添加依赖(Gradle示例):
groovy复制implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5',
'io.jsonwebtoken:jjwt-jackson:0.11.5'
配置JWT工具类核心方法:
java复制public class JwtUtil {
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.claim("roles", user.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SECRET_KEY)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
}
3.2 认证过滤器实现
创建JWT认证过滤器处理HTTP请求:
java复制public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
try {
String token = authHeader.substring(7);
Claims claims = JwtUtil.parseToken(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
claims.getSubject(),
null,
((List<?>) claims.get("roles")).stream()
.map(authority -> new SimpleGrantedAuthority((String) authority))
.collect(Collectors.toList())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
}
filterChain.doFilter(request, response);
}
}
3.3 Spring Security配置
关键安全配置要点:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
4. 高级实践与安全加固
4.1 令牌刷新机制
短期访问令牌(1小时)+ 长期刷新令牌(7天)是推荐方案:
java复制// 生成双令牌
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", generateToken(user, 3600000));
tokens.put("refresh_token", generateToken(user, 604800000));
// 刷新接口实现
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest request) {
try {
Claims claims = JwtUtil.parseToken(request.getRefreshToken());
if (!"REFRESH".equals(claims.get("type"))) {
throw new JwtException("Invalid token type");
}
return ok(generateToken(loadUser(claims.getSubject()), 3600000));
} catch (JwtException e) {
return unauthorized();
}
}
4.2 黑名单与令牌吊销
虽然JWT本身无状态,但关键操作仍需黑名单:
redis复制# Redis黑名单示例
SETEX "blacklist:"+token 3600 "revoked" # 设置1小时过期
4.3 安全防护最佳实践
- HTTPS强制:全站HTTPS是基础前提
- HttpOnly Cookie:防止XSS攻击窃取令牌
- 短期有效期:访问令牌不超过1小时
- 密钥轮换:定期更换签名密钥(如每月)
- Claims最小化:payload只放必要信息
- 日志脱敏:避免令牌出现在日志中
我在实际项目中最容易忽视的是第6点,曾因日志泄露导致安全事件。现在会统一配置Logback的%replace转换器:
xml复制<pattern>%replace(%msg){'Bearer \w+\.\w+\.\w+', 'Bearer [MASKED]'}</pattern>
5. 常见问题排查指南
5.1 签名验证失败
典型错误:
code复制io.jsonwebtoken.security.SignatureException: JWT signature does not match
排查步骤:
- 确认服务端密钥未变更
- 检查令牌是否被截断(特别是URL传输时)
- 验证算法是否一致(HS256 vs RS256)
5.2 令牌过期处理
前端应实现自动刷新流程:
javascript复制axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401 &&
!error.config.url.includes('/refresh')) {
return refreshToken().then(() => {
error.config.headers['Authorization'] = `Bearer ${getNewToken()}`;
return axios.request(error.config);
});
}
return Promise.reject(error);
});
5.3 性能优化技巧
- 使用非对称加密时,缓存公钥解析结果
- 避免在JWT中存储大体积数据
- 对频繁访问的接口实现本地校验缓存
在日活百万级的系统中,我们通过以下优化使JWT验证性能提升40%:
java复制// 缓存已验证的令牌指纹
LoadingCache<String, Boolean> tokenCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> {
Claims claims = JwtUtil.parseToken(key);
return !isRevoked(claims.getId());
});
