JWT 认证与登录注销
约 1414 字大约 5 分钟
布欧-Lewyon
2026-05-16
首页 › Spring Boot › 安全入门 › JWT 认证与登录注销
JSON Web Token(JWT)是无状态认证的主流方案——服务端不保存 Session,认证信息编码在 Token 中由客户端持有。本节覆盖登录签发 Token、请求鉴权、注销与 Token 刷新。
依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>也可以选 io.jsonwebtoken:jjwt 或 Spring Security 自带的 oauth2-jose,本处以 java-jwt 为例。
JWT 工具类
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400000}")
private long expiration; // 默认 24 小时
private final Algorithm algorithm;
public JwtUtil(@Value("${jwt.secret}") String secret) {
this.secret = secret;
this.algorithm = Algorithm.HMAC256(secret);
}
// 生成 Token
public String generateToken(Long userId, String username, List<String> roles) {
return JWT.create()
.withSubject(userId.toString())
.withClaim("username", username)
.withClaim("roles", roles)
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + expiration))
.sign(algorithm);
}
// 验证 Token
public DecodedJWT verifyToken(String token) {
return JWT.require(algorithm).build().verify(token);
}
// 从 Token 中提取用户 ID
public Long getUserId(String token) {
return Long.valueOf(verifyToken(token).getSubject());
}
// 从 Token 中提取角色
public List<String> getRoles(String token) {
return verifyToken(token).getClaim("roles").asList(String.class);
}
}jwt:
secret: your-256-bit-secret-here-change-in-production
expiration: 86400000生产意识:
jwt.secret必须通过环境变量注入(${JWT_SECRET}),禁止硬编码在配置文件中。密钥长度至少 256 位(HMAC-SHA256)。
登录接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// ① Spring Security 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
// ② 获取用户详情与角色
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
// ③ 签发 Token
long userId = extractUserId(userDetails);
String token = jwtUtil.generateToken(userId, request.getUsername(), roles);
// ④ 返回 Token(客户端可存 localStorage / 内存 / httpOnly Cookie)
return Result.success(new LoginResponse(token, jwtUtil.getExpiration()));
}
}public record LoginRequest(@NotBlank String username, @NotBlank String password) {}
public record LoginResponse(String token, long expiresIn) {}JWT 认证过滤器
将 JWT 校验嵌入 Spring Security 的过滤器链:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
// 白名单路径——不校验 Token
private static final List<String> WHITE_LIST = List.of(
"/api/auth/login", "/api/auth/refresh", "/api/public/");
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String path = request.getRequestURI();
// 白名单跳过
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
chain.doFilter(request, response);
return;
}
// 从请求头中提取 Token
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
DecodedJWT decoded = jwtUtil.verifyToken(token);
String username = decoded.getClaim("username").asString();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (JWTVerificationException e) {
log.warn("JWT 验证失败: {}", e.getMessage());
// Token 无效,不设置认证——由 AuthorizationFilter 返回 403
}
chain.doFilter(request, response);
}
}注册到安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}注销
无状态 JWT 无法像 Session 一样"销毁"服务端状态。常见注销方案:
黑名单方案(推荐)
将注销的 Token 加入 Redis 黑名单,TTL 设为 Token 剩余有效期:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.replace("Bearer ", "");
// 获取 Token 剩余有效期
DecodedJWT decoded = jwtUtil.verifyToken(token);
long remainTtl = decoded.getExpiresAt().getTime() - System.currentTimeMillis();
if (remainTtl > 0) {
// 加入黑名单
redisTemplate.opsForValue().set(
"blacklist:token:" + decoded.getId(),
"1",
Duration.ofMillis(remainTtl));
}
return Result.success(null);
}
}过滤器增加黑名单检查:
// 在 JwtAuthenticationFilter 中
String jti = decoded.getId();
String blacklisted = redisTemplate.opsForValue().get("blacklist:token:" + jti);
if (blacklisted != null) {
throw new JWTVerificationException("Token 已注销");
}短 Token + 长 Refresh Token
@PostMapping("/refresh")
public Result<LoginResponse> refresh(@RequestBody RefreshRequest request) {
// 验证 Refresh Token
DecodedJWT decoded = jwtUtil.verifyToken(request.refreshToken());
// 检查是否已注销(黑名单)
if (isBlacklisted(decoded.getId())) {
return Result.error(401, "Refresh Token 已失效");
}
// 签发新 Access Token
String username = decoded.getClaim("username").asString();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList();
String newToken = jwtUtil.generateToken(userId, username, roles);
return Result.success(new LoginResponse(newToken, jwtUtil.getExpiration()));
}Cookie vs Authorization Header
| 方式 | 存储位置 | CSRF 风险 | 适用场景 |
|---|---|---|---|
| Authorization Header | 前端内存/localStorage | 无(浏览器不会自动附加) | 前后端分离 SPA(推荐) |
| httpOnly Cookie | 浏览器 Cookie | 需 CSRF 防护 | 服务端渲染、非 SPA |
| 混合 | RefreshToken 存 Cookie,AccessToken 存内存 | 低 | 安全要求较高的场景 |
// httpOnly Cookie 方式设置 Token
@PostMapping("/login")
public Result<Void> loginWithCookie(@Valid @RequestBody LoginRequest request,
HttpServletResponse response) {
// ... 认证逻辑 ...
String token = jwtUtil.generateToken(...);
Cookie cookie = new Cookie("access_token", token);
cookie.setHttpOnly(true);
cookie.setSecure(true); // 仅 HTTPS
cookie.setPath("/");
cookie.setMaxAge((int) (jwtUtil.getExpiration() / 1000));
cookie.setAttribute("SameSite", "Strict");
response.addCookie(cookie);
return Result.success(null);
}生产意识:localStorage 存储 Token 存在 XSS 风险——如果页面被注入恶意脚本,Token 可被窃取。推荐 AccessToken 存内存变量(刷新后丢失),RefreshToken 存 httpOnly Cookie。业务安全要求较高时,考虑 BFF(Backend for Frontend)架构,由网关层统一管理 Token。
小结
- JWT 无状态认证:登录签发 → 请求携带 → 过滤器校验 → 安全上下文写入。
- 注销方案:黑名单(Redis)或短 Token + Refresh Token 双 Token 模式。
- Token 传递方式:Authorization Header(SPA 推荐)或 httpOnly Cookie(SSR 场景)。
- RBAC 权限模型已在
spring-security-basics.md中通过hasRole/hasAuthority实现。 - 易错:JWT 一旦签发无法在服务端主动失效——注销必须依赖黑名单。
JwtAuthenticationFilter必须在UsernamePasswordAuthenticationFilter之前执行,否则表单登录过滤器会拦截/login以外的请求。Secret 密钥轮换时,旧 Token 会立即失效——通过 JWKS(JSON Web Key Set)实现密钥的平滑轮换。 - 思考任务:实现登录签发 JWT +
JwtAuthenticationFilter鉴权;配置白名单/api/auth/**不校验 Token;实现 Refresh Token 刷新机制。
下一节:OAuth2.0 集成
