JWT 认证详解
项目使用 JJWT 0.13.0 实现 JWT 认证,Token 中包含用户名、用户ID和角色信息。
🔧 配置
application.yaml
app:
security:
jwt-secret: ${JWT_SECRET} # 至少 256 位
jwt-expiration: 7200000 # 2小时(毫秒)
SecurityProperties.java
@Data
@Configuration
@ConfigurationProperties(prefix = "app.security")
public class SecurityProperties {
private String jwtSecret;
private Long jwtExpiration = 7200000L;
}
🔑 JwtTokenProvider
位置: blog-common/src/main/java/com/blog/common/security/JwtTokenProvider.java
生成 Token
public String generateToken(UserDetails userDetails, Long userId) {
Map<String, Object> claims = new HashMap<>();
// 提取角色
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
claims.put("roles", roles);
claims.put("userId", userId);
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey())
.compact();
}
Token 结构
{
"sub": "testuser", // 用户名
"userId": 1, // 用户ID
"roles": ["ROLE_USER"], // 角色列表
"iat": 1702382400, // 签发时间
"exp": 1702389600 // 过期时间
}
验证 Token
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (Exception e) {
log.error("JWT Token 验证失败: {}", e.getMessage());
return false;
}
}
提取信息
// 提取用户名
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
// 提取用户ID
public Long getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
Object userId = claims.get("userId");
if (userId instanceof Integer) {
return ((Integer) userId).longValue();
}
return (Long) userId;
}
// 提取角色
public List<String> getRolesFromToken(String token) {
return (List<String>) getClaimsFromToken(token).get("roles");
}
🔍 JwtAuthenticationFilter
位置: blog-application/src/main/java/com/blog/security/JwtAuthenticationFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求头提取 Token
String jwt = getJwtFromRequest(request);
// 2. 验证 Token
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// 3. 解析用户信息
String username = tokenProvider.getUsernameFromToken(jwt);
Long userId = tokenProvider.getUserIdFromToken(jwt);
List<String> roles = tokenProvider.getRolesFromToken(jwt);
// 4. 构建权限列表
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
authentication.setDetails(
new JwtAuthenticationDetails(request, userId));
// 6. 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.error("无法设置用户认证: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
📦 JwtAuthenticationDetails
存储额外的用户信息(如 userId):
@Getter
public class JwtAuthenticationDetails extends WebAuthenticationDetails {
private final Long userId;
public JwtAuthenticationDetails(HttpServletRequest request, Long userId) {
super(request);
this.userId = userId;
}
}
🔐 使用示例
登录获取 Token
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"Password123!"}'
{
"code": 0,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"tokenType": "Bearer",
"expiresIn": 7200
}
}
携带 Token 访问 API
curl -X GET http://localhost:8080/api/v1/users/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
⚠️ 安全最佳实践
1. JWT Secret 配置
# 生成安全的 256 位密钥
openssl rand -base64 32
# 设置环境变量
export JWT_SECRET="生成的密钥"
生产环境
- 必须通过环境变量注入
JWT_SECRET - 密钥长度至少 256 位(32 字节)
- 不要硬编码在配置文件中
2. Token 传递
# ✅ 正确:使用 Authorization Header
Authorization: Bearer eyJhbGciOiJ...
# ❌ 错误:URL 参数传递
?token=eyJhbGciOiJ...
3. JJWT API 版本
项目使用 JJWT 0.12+ 现代 API:
| 新 API | 旧 API(已废弃) |
|---|---|
Keys.hmacShaKeyFor() | .setSigningKey(String) |
.verifyWith() | .setSigningKey() |
.parseSignedClaims() | .parseClaimsJws() |
📚 延伸阅读
- Security 概述 — 三链架构
- SecurityUtils 工具类 — 获取当前用户