在微服务和前后端分离架构下,基于 Token 的认证方案已经成为主流。JWT(JSON Web Token)凭借其无状态、跨语言支持、易于扩展等特性,成为 RESTful API 认证的首选方案。本文将从 Spring Security 的过滤器链机制出发,手把手教你搭建一套完整的 JWT 认证体系。
📖 目录
1. Spring Security 过滤器链
Spring Security 的核心是一组过滤器链(Filter Chain)。每个 HTTP 请求进入后会依次经过一系列过滤器,每个过滤器负责一项安全检查。理解这个链条是配置任何认证方案的基础。
默认的过滤器链中包含的关键过滤器(按顺序):
- SecurityContextPersistenceFilter:从 Session 或请求头中恢复 SecurityContext
- UsernamePasswordAuthenticationFilter:处理表单登录认证
- BasicAuthenticationFilter:处理 HTTP Basic 认证
- ExceptionTranslationFilter:捕获安全异常并处理
- FilterSecurityInterceptor:执行访问控制决策
当我们集成 JWT 时,需要在过滤器链的合适位置插入自定义的 JWT 认证过滤器。通常的做法是将 JwtAuthenticationFilter 放在 UsernamePasswordAuthenticationFilter 之前,这样每个请求都会先经过 JWT 验证:
请求进入
│
├── SecurityContextPersistenceFilter
├── ... 其他过滤器 ...
├── JwtAuthenticationFilter (自定义,插入在此处)
├── UsernamePasswordAuthenticationFilter
├── ExceptionTranslationFilter
└── FilterSecurityInterceptor
│
└── 放行或拒绝
2. JWT 结构与原理
JWT 由三部分组成,用点号分隔:
Header.Payload.Signature
| 部分 | 内容 | 示例 |
|---|---|---|
| Header | 算法类型和 Token 类型 | {"alg":"HS256","typ":"JWT"} |
| Payload | 声明数据(Claims),如用户 ID、角色、过期时间 | {"sub":"1001","role":"admin","exp":1718000000} |
| Signature | 对前两部分的签名,防止篡改 | HMACSHA256(base64(header)+"."+base64(payload), secret) |
JWT 的核心优势是无状态。服务端不需要存储会话信息,所有用户身份信息都编码在 Token 本身中。但这也意味着 Token 一旦签发就无法失效——除非引入黑名单机制。
注意:不要在 JWT 的 Payload 中存储敏感信息(如密码),因为 Payload 只是 Base64 编码而非加密,任何人都可以解码读取。
3. 项目搭建与依赖配置
创建一个 Spring Boot 项目,在 pom.xml 中添加以下核心依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>
这里推荐使用 jjwt 库(0.12.x 版本),它是 Java 生态中最流行的 JWT 实现库之一,API 设计清晰,支持多种签名算法。相比早期的 com.auth0:java-jwt 或手动 Base64 拼接的方式,jjwt 提供了更完整的 JWT 标准实现。
4. SecurityConfig 配置详解
SecurityConfig 是整个认证体系的骨架。我们需要在这里配置过滤链、CSRF、CORS、异常处理等内容。这是最核心的配置类:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthFilter,
UserDetailsService userDetailsService) {
this.jwtAuthFilter = jwtAuthFilter;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(前后端分离场景不需要)
.csrf(AbstractHttpConfigurer::disable)
// CORS 配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 配置无需认证的公开接口
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(unauthorizedHandler())
.accessDeniedHandler(accessDeniedHandler())
)
// 使用无状态 Session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 插入 JWT 过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
几个关键要点:
- 禁用 CSRF:前后端分离使用 Token 认证,CSRF 防护由 Token 本身提供
- STATELESS Session:不使用 HTTP Session 存储认证信息
- addFilterBefore:确保 JWT 过滤器在 UsernamePasswordAuthenticationFilter 之前执行
- BCryptPasswordEncoder:推荐使用 BCrypt 算法加密密码,不要使用 MD5 或 SHA
5. JWT 工具类实现
JwtUtil 负责 Token 的生成、解析和验证。我们使用 jjwt 库,将密钥和过期时间配置在 application.yml 中:
@Component
public class JwtUtil {
private final SecretKey secretKey;
private final long expirationMs;
public JwtUtil(
@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration-ms}") long expirationMs) {
// 使用足够长的密钥(至少256位)
byte[] keyBytes = Keys.secretKeyFor(SignatureAlgorithm.HS256)
.getEncoded();
this.secretKey = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
this.expirationMs = expirationMs;
}
// 生成 Token
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(secretKey)
.compact();
}
// 从 Token 中提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 提取某个声明
public <T> T extractClaim(String token,
Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 验证 Token 是否有效
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())
&& !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration)
.before(new Date());
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
关于密钥管理的几点建议:
- 生产环境不要将密钥硬编码在代码中,应通过环境变量或配置中心注入
- HS256 要求密钥长度至少 256 位,密钥过短会抛出异常
- 可以考虑引入密钥轮换机制,定期更换签名密钥
- Token 过期时间不宜过长,通常 15-30 分钟,配合 Refresh Token 使用
6. 自定义 JWT 过滤器
JwtAuthenticationFilter 继承 OncePerRequestFilter,确保每个请求只经过一次过滤。这是 JWT 认证的核心组件:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(
JwtUtil jwtUtil,
UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
// 没有 Authorization 头或不是 Bearer 开头,直接放行
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 提取 Token
final String jwt = authHeader.substring(7);
try {
final String username = jwtUtil.extractUsername(jwt);
// 当前 SecurityContext 中没有认证信息
if (username != null
&& SecurityContextHolder.getContext()
.getAuthentication() == null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
// 验证 Token
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request));
// 设置到 SecurityContext 中
SecurityContextHolder.getContext()
.setAuthentication(authToken);
}
}
} catch (Exception e) {
// Token 无效或过期,清除上下文
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
这个过滤器的工作流程:
- 从请求头中提取
Authorization: Bearer <token> - 调用 JwtUtil 解析 Token 获取用户名
- 通过 UserDetailsService 加载用户信息
- 验证 Token 是否有效且未过期
- 构造
UsernamePasswordAuthenticationToken并设置到 SecurityContext - 后续过滤器即可通过 SecurityContext 获取当前用户信息
7. 完整认证流程
将前面的组件串联起来,就构成了完整的 JWT 认证流程。下面是登录接口的实现:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public AuthController(
AuthenticationManager authenticationManager,
JwtUtil jwtUtil,
UserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(
@RequestBody LoginRequest request) {
// 1. 认证用户名密码
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(),
request.password()
)
);
// 2. 加载用户信息
UserDetails userDetails =
userDetailsService.loadUserByUsername(request.username());
// 3. 生成 JWT Token
String token = jwtUtil.generateToken(userDetails);
// 4. 返回 Token
return ResponseEntity.ok(new LoginResponse(token));
}
}
完整的请求处理流程如下:
- 客户端发起登录请求
POST /api/auth/login,提交用户名和密码 - 服务器验证用户名密码,成功后生成 JWT Token 返回给客户端
- 客户端保存 Token(通常存储在 localStorage 或 HttpOnly Cookie 中)
- 后续请求在 Authorization 头中携带 Token
- JwtAuthenticationFilter 拦截请求,解析并验证 Token
- 验证通过后设置 SecurityContext,请求放行到目标接口
- 目标接口通过
@AuthenticationPrincipal获取当前用户信息
对于未认证或 Token 过期的请求,我们需要提供友好的错误响应。下面是认证入口点的实现:
@Component
public class UnauthorizedEntryPoint
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> body = new HashMap<>();
body.put("code", 401);
body.put("message", "未认证,请先登录");
body.put("timestamp", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper();
response.getWriter()
.write(mapper.writeValueAsString(body));
}
}
8. 认证方案对比
在实际项目中选择认证方案时,需要根据业务场景权衡。下面是三种常见方案的对比:
| 对比维度 | Session 认证 | JWT 认证 | OAuth2 认证 |
|---|---|---|---|
| 状态 | 有状态(服务端存储 Session) | 无状态(Token 自包含信息) | 无状态(Token 自包含信息) |
| 扩展性 | 差,需要 Session 共享或粘滞 Session | 好,天然支持水平扩展 | 好,支持授权码、客户端等多种模式 |
| Token 撤销 | 简单,清除 Session 即可 | 复杂,需要黑名单机制 | 依赖授权服务器策略 |
| 跨域支持 | 需要配置跨域 Session 共享 | 天然支持,Token 在请求头中传递 | 天然支持 |
| 安全风险 | CSRF、Session 固定攻击 | Token 泄露、重放攻击 | 授权码拦截、Token 泄露 |
| 实现复杂度 | 低,Spring Security 默认支持 | 中,需自行实现过滤器 | 高,需对接授权服务器 |
| 适用场景 | 传统单体应用、服务端渲染 | 前后端分离、移动端 API | 第三方登录、微服务间授权 |
在实际项目中,这三种方案并非互斥。一个典型的微服务架构可能会同时使用多种方案:内部服务间使用 JWT 做服务认证,对外开放的 API 使用 OAuth2,老系统兼容保留 Session 认证。选择合适的认证方案,需要在安全、性能和开发成本之间找到平衡。
JWT 认证是目前前后端分离项目中最主流的选择。它结合了 Session 的简单易用和 OAuth2 的扩展性优点,配合 Spring Security 强大的过滤器链机制,可以构建出一套既安全又灵活的认证体系。希望本文能帮你顺利地在项目中落地 JWT 认证方案。