JWT 认证
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。在前后端分离架构中,JWT 已成为最流行的认证方案之一。本章将详细介绍 JWT 的原理、配置以及在 Spring Security 中的实现。
JWT 基础
什么是 JWT?
JWT 是一种紧凑、自包含的令牌格式,由三部分组成,用点号分隔:
xxxxx.yyyyy.zzzzz
这三部分分别是:
Header(头部):包含令牌类型和签名算法
{
"alg": "HS256",
"typ": "JWT"
}
Payload(载荷):包含声明(claims),即用户信息和元数据
{
"sub": "user123",
"name": "张三",
"iat": 1516239022,
"exp": 1516242622
}
Signature(签名):对前两部分的签名,用于验证令牌完整性
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT vs Session
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 扩展性 | 无状态,易于水平扩展 | 需要 Session 共享 |
| 跨域支持 | 天然支持 | 需要额外配置 |
| 安全性 | 无法主动失效 | 可即时失效 |
| 载荷大小 | 不宜过大 | 无限制 |
| 适用场景 | 移动端、前后端分离 | 传统 Web 应用 |
JWT 认证流程
- 用户使用用户名密码登录
- 服务端验证成功后生成 JWT 返回给客户端
- 客户端存储 JWT(通常在 localStorage 或 Cookie)
- 后续请求在 Authorization 头中携带 JWT
- 服务端验证 JWT 并提取用户信息
Spring Security 集成 JWT
添加依赖
首先添加必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
JWT 工具类
创建一个工具类来处理 JWT 的生成和验证:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:86400000}") // 默认 24 小时
private long validityInMilliseconds;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成 Token
public String createToken(String username, Collection<? extends GrantedAuthority> authorities) {
Claims claims = Jwts.claims().subject(username).build();
// 添加权限信息
List<String> roles = authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.claims(claims)
.issuedAt(now)
.expiration(validity)
.signWith(getSigningKey())
.compact();
}
// 从 Token 获取用户名
public String getUsername(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
// 获取权限列表
@SuppressWarnings("unchecked")
public Collection<? extends GrantedAuthority> getAuthorities(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
List<String> roles = claims.get("roles", List.class);
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
// 验证 Token
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// 日志记录
return false;
}
}
}
JWT 认证过滤器
创建过滤器来拦截请求并验证 JWT:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider,
UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 从请求头获取 Token
String token = resolveToken(request);
// 2. 验证 Token
if (token != null && jwtTokenProvider.validateToken(token)) {
try {
// 3. 从 Token 获取用户信息
String username = jwtTokenProvider.getUsername(token);
// 4. 加载用户详情
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 6. 设置到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
安全配置
配置 Spring Security 使用 JWT 认证:
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
public JwtSecurityConfig(JwtTokenProvider jwtTokenProvider,
UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(无状态应用不需要)
.csrf(csrf -> csrf.disable())
// 配置 Session 管理:无状态
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
UsernamePasswordAuthenticationFilter.class
)
// 配置异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证入口点
处理未认证请求的响应:
@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);
String json = String.format(
"{\"error\": \"Unauthorized\", \"message\": \"%s\", \"path\": \"%s\"}",
authException.getMessage(),
request.getRequestURI()
);
response.getWriter().write(json);
}
}
认证控制器
登录接口
创建登录接口返回 JWT:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
public AuthController(AuthenticationManager authenticationManager,
JwtTokenProvider jwtTokenProvider) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
// 1. 创建认证令牌
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
request.username(),
request.password()
);
// 2. 执行认证
Authentication authentication = authenticationManager.authenticate(authToken);
// 3. 生成 JWT
String token = jwtTokenProvider.createToken(
authentication.getName(),
authentication.getAuthorities()
);
// 4. 返回 Token
return ResponseEntity.ok(new JwtResponse(token));
} catch (BadCredentialsException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse("用户名或密码错误"));
}
}
public record LoginRequest(String username, String password) {}
public record JwtResponse(String token) {}
public record ErrorResponse(String error) {}
}
注册接口
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
// 1. 检查用户是否存在
if (userService.existsByUsername(request.username())) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("用户名已存在"));
}
// 2. 创建用户
User user = userService.createUser(request);
// 3. 自动登录,生成 Token
String token = jwtTokenProvider.createToken(
user.getUsername(),
user.getAuthorities()
);
return ResponseEntity.ok(new JwtResponse(token));
}
Token 刷新机制
为什么需要刷新机制?
JWT 一旦签发,在过期前无法撤销。为了平衡安全性和用户体验,通常采用:
- Access Token:有效期短(如 15 分钟),用于接口访问
- Refresh Token:有效期长(如 7 天),用于刷新 Access Token
实现 Refresh Token
@Component
public class JwtTokenProvider {
@Value("${jwt.access-token-validity:900000}") // 15 分钟
private long accessTokenValidity;
@Value("${jwt.refresh-token-validity:604800000}") // 7 天
private long refreshTokenValidity;
// 生成 Access Token
public String createAccessToken(String username,
Collection<? extends GrantedAuthority> authorities) {
return createToken(username, authorities, accessTokenValidity);
}
// 生成 Refresh Token
public String createRefreshToken(String username) {
Date now = new Date();
Date validity = new Date(now.getTime() + refreshTokenValidity);
return Jwts.builder()
.subject(username)
.issuedAt(now)
.expiration(validity)
.claim("type", "refresh")
.signWith(getSigningKey())
.compact();
}
// 判断是否是 Refresh Token
public boolean isRefreshToken(String token) {
Claims claims = getClaims(token);
return "refresh".equals(claims.get("type", String.class));
}
}
刷新接口
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshTokenRequest request) {
String refreshToken = request.refreshToken();
// 1. 验证 Refresh Token
if (!jwtTokenProvider.validateToken(refreshToken) ||
!jwtTokenProvider.isRefreshToken(refreshToken)) {
return ResponseEntity.status(401)
.body(new ErrorResponse("无效的刷新令牌"));
}
// 2. 获取用户信息
String username = jwtTokenProvider.getUsername(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 3. 生成新的 Access Token
String newAccessToken = jwtTokenProvider.createAccessToken(
username,
userDetails.getAuthorities()
);
return ResponseEntity.ok(new JwtResponse(newAccessToken));
}
Token 黑名单
为什么需要黑名单?
当用户登出或 Token 泄露时,需要让 Token 失效。由于 JWT 无状态,需要维护黑名单来阻止特定 Token 的使用。
Redis 实现
@Component
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:";
public TokenBlacklistService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 将 Token 加入黑名单
public void addToBlacklist(String token, long expirationTime) {
String key = BLACKLIST_PREFIX + token;
long ttl = expirationTime - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(key, "1", ttl, TimeUnit.MILLISECONDS);
}
}
// 检查 Token 是否在黑名单中
public boolean isBlacklisted(String token) {
String key = BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}
在过滤器中检查黑名单
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
// 检查黑名单
if (tokenBlacklistService.isBlacklisted(token)) {
response.setStatus(401);
response.getWriter().write("{\"error\": \"Token 已失效\"}");
return;
}
// 正常验证流程...
}
filterChain.doFilter(request, response);
}
登出时加入黑名单
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
String token = resolveToken(request);
if (token != null) {
// 获取 Token 过期时间
Claims claims = jwtTokenProvider.getClaims(token);
long expiration = claims.getExpiration().getTime();
// 加入黑名单
tokenBlacklistService.addToBlacklist(token, expiration);
}
return ResponseEntity.ok(new MessageResponse("登出成功"));
}
安全最佳实践
密钥管理
密钥应该足够长且随机,不要硬编码在代码中:
# application.yml
jwt:
secret: ${JWT_SECRET:your-very-long-secret-key-at-least-256-bits}
expiration: 86400000
生产环境应从环境变量或密钥管理服务获取:
# 生成安全密钥
openssl rand -base64 64
Token 存储
前端存储 JWT 的选择:
| 存储方式 | XSS 风险 | CSRF 风险 | 推荐度 |
|---|---|---|---|
| localStorage | 高 | 无 | 低 |
| sessionStorage | 高 | 无 | 低 |
| HttpOnly Cookie | 无 | 高 | 高(配合 CSRF 防护) |
| 内存变量 | 无 | 无 | 高(但刷新页面丢失) |
推荐使用 HttpOnly Cookie 存储:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request,
HttpServletResponse response) {
// ... 认证逻辑
String token = jwtTokenProvider.createToken(username, authorities);
// 设置 HttpOnly Cookie
Cookie cookie = new Cookie("jwt", token);
cookie.setHttpOnly(true);
cookie.setSecure(true); // 仅 HTTPS
cookie.setPath("/");
cookie.setMaxAge(24 * 60 * 60);
response.addCookie(cookie);
return ResponseEntity.ok().build();
}
Token 内容安全
不要在 JWT 中存储敏感信息:
// 错误:存储敏感信息
claims.put("password", user.getPassword());
claims.put("idCard", user.getIdCard());
// 正确:只存储必要信息
claims.setSubject(user.getUsername());
claims.put("userId", user.getId());
小结
本章详细介绍了 JWT 认证的实现:
- JWT 由 Header、Payload、Signature 三部分组成,适合无状态认证场景
- Spring Security 集成 JWT 需要自定义认证过滤器和 Token 提供者
- Access Token + Refresh Token 机制平衡安全性和用户体验
- Token 黑名单用于处理登出和 Token 泄露场景
- 密钥管理、Token 存储、内容安全是重要的安全实践
JWT 认证是现代 Web 应用的重要认证方式,掌握其原理和实现对于构建安全的 API 至关重要。下一章将学习 OAuth2 集成,了解更复杂的授权场景。