在微服务和前后端分离架构下,基于 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);
    }
}

这个过滤器的工作流程:

  1. 从请求头中提取 Authorization: Bearer <token>
  2. 调用 JwtUtil 解析 Token 获取用户名
  3. 通过 UserDetailsService 加载用户信息
  4. 验证 Token 是否有效且未过期
  5. 构造 UsernamePasswordAuthenticationToken 并设置到 SecurityContext
  6. 后续过滤器即可通过 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));
    }
}

完整的请求处理流程如下:

  1. 客户端发起登录请求 POST /api/auth/login,提交用户名和密码
  2. 服务器验证用户名密码,成功后生成 JWT Token 返回给客户端
  3. 客户端保存 Token(通常存储在 localStorage 或 HttpOnly Cookie 中)
  4. 后续请求在 Authorization 头中携带 Token
  5. JwtAuthenticationFilter 拦截请求,解析并验证 Token
  6. 验证通过后设置 SecurityContext,请求放行到目标接口
  7. 目标接口通过 @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 认证方案。