一、核心概念

1.1 什么是 JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。它通常用于身份验证和信息交换。

1.2 JWT 结构

JWT 由三部分组成,用点(.)分隔:

xxxxx.yyyyy.zzzzz
  • Header(头部):包含令牌类型和使用的签名算法(如 HS256)。
  • Payload(负载):包含声明(Claims),如用户信息、过期时间等。
  • Signature(签名):对前两部分的签名,确保令牌未被篡改。

1.3 无状态认证(Stateless Authentication)

  • 有状态:服务器需要维护 Session 状态(如存储在内存、Redis)。
  • 无状态:服务器不存储会话信息,每次请求都携带 JWT,服务器通过验证 JWT 的签名来确认用户身份。

1.4 核心优势

  • 可扩展性:适合分布式系统和微服务架构。
  • 跨域支持:易于在不同域之间传递。
  • 自包含:Token 包含所有必要信息。

二、详细操作步骤(适合快速实践)

步骤 1:创建 Spring Boot 项目并添加依赖

使用 start.spring.io 创建项目,添加以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <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.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <!-- 数据库支持(可选) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

注意:JJWT 0.11+ 版本将 API、实现和 Jackson 支持分离。

步骤 2:创建 JWT 工具类 JwtUtil.java

package com.example.demo.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

    // 密钥(生产环境应使用更安全的密钥,如从配置中心获取)
    @Value("${jwt.secret:mySecretKeyThatIsAtLeast32CharactersLong}")
    private String secret;

    // Token 有效期(毫秒)
    @Value("${jwt.expiration:86400000}") // 24小时
    private long expiration;

    // 获取签名密钥
    private Key getSignKey() {
        return Keys.hmacShaKeyFor(secret.getBytes());
    }

    // 从 Token 中提取用户名
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // 从 Token 中提取过期时间
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    // 提取单个声明
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    // 解析 Token 获取所有声明
    private Claims extractAllClaims(String token) {
        try {
            return Jwts.parser()
                .verifyWith(getSignKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
        } catch (JwtException e) {
            throw new IllegalArgumentException("Invalid JWT token", e);
        }
    }

    // 检查 Token 是否过期
    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    // 生成 Token
    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, username);
    }

    // 生成 Token(带自定义声明)
    public String generateToken(Map<String, Object> claims, String username) {
        return createToken(claims, username);
    }

    // 创建 Token
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
            .claims(claims)
            .subject(subject)
            .issuedAt(new Date(System.currentTimeMillis()))
            .expiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSignKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    // 验证 Token 是否有效
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

步骤 3:创建用户实体和仓库(可选,用于数据库用户)

// UserEntity.java
@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role; // 如 "ROLE_USER"

    // getters and setters
}

// UserRepository.java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Optional<UserEntity> findByUsername(String username);
}

步骤 4:实现 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole().replace("ROLE_", ""))
            .build();
    }
}

步骤 5:创建 JWT 认证过滤器 JwtRequestFilter.java

package com.example.demo.filter;

import com.example.demo.util.JwtUtil;
import com.example.demo.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // 检查 Authorization 头是否存在且以 "Bearer " 开头
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7); // 去除 "Bearer " 前缀
            try {
                username = jwtUtil.extractUsername(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }

        // 如果用户名存在且当前未认证
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // 验证 Token
            if (jwtUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 将认证信息放入 SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

步骤 6:配置安全配置类 SecurityConfig.java

package com.example.demo.config;

import com.example.demo.filter.JwtRequestFilter;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // JWT 通常与 CSRF 结合使用较复杂,无状态可禁用
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/authenticate", "/public/**").permitAll() // 登录接口放行
                .anyRequest().authenticated() // 其他请求需要认证
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
            )
            // 在 UsernamePasswordAuthenticationFilter 之前添加 JWT 过滤器
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

步骤 7:创建认证控制器 AuthController.java

package com.example.demo.controller;

import com.example.demo.util.JwtUtil;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    // 登录接口,返回 JWT
    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        } catch (Exception e) {
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails.getUsername());

        Map<String, String> response = new HashMap<>();
        response.put("token", jwt);
        return ResponseEntity.ok(response);
    }

    // 请求体类
    public static class AuthenticationRequest {
        private String username;
        private String password;

        // getters and setters
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }
}

步骤 8:创建测试控制器

@RestController
public class TestController {

    @GetMapping("/public/hello")
    public String hello() {
        return "Hello, Public!";
    }

    @GetMapping("/secure/data")
    @PreAuthorize("hasRole('USER')")
    public String secureData() {
        return "This is secure data!";
    }

    @GetMapping("/admin/stats")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminStats() {
        return "Admin only!";
    }
}

步骤 9:配置文件 application.yml

server:
  port: 8080

jwt:
  secret: mySuperSecretKeyThatIsAtLeast32CharactersLong
  expiration: 86400000 # 24 hours in milliseconds

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

步骤 10:运行与测试

  1. 启动应用
  2. 获取 Token
    • 发送 POST 请求到 http://localhost:8080/authenticate
    • Body:
      {
        "username": "user",
        "password": "password"
      }
      
    • 响应:
      { "token": "eyJhbGciOiJIUzI1NiJ9.xxxxx" }
      
  3. 访问受保护资源
    • 发送 GET 请求到 http://localhost:8080/secure/data
    • Header: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx
    • 成功返回数据。
  4. 访问未授权资源
    • user 的 Token 访问 /admin/stats → 403 Forbidden。

三、常见错误与解决方案

错误现象 原因分析 解决方案
Invalid JWT token 签名密钥不匹配、Token 格式错误 检查 jwt.secret 配置;确认 Token 以 Bearer 开头
JWT Token has expired Token 已过期 重新登录获取新 Token;调整 jwt.expiration
401 Unauthorized 未提供 Token 或 Token 无效 确保请求头包含 Authorization: Bearer <token>
No 'Access-Control-Allow-Origin' CORS 问题 配置 CORS 过滤器或使用 @CrossOrigin
Unable to inject AuthenticationManager 未正确暴露 Bean 确保在 SecurityConfig 中定义 @Bean AuthenticationManager

四、注意事项

  1. 密钥安全
    • 生产环境密钥必须足够长(至少 32 字符)且保密。
    • 避免硬编码,使用环境变量或配置中心。
  2. Token 过期
    • 设置合理的过期时间(如 15-30 分钟)。
    • 实现 Refresh Token 机制(本文未涉及)。
  3. CSRF 与 JWT
    • 无状态 JWT 通常不需要传统 CSRF 保护,但需防范 XSS。
  4. 敏感信息
    • 不要在 JWT Payload 中存储敏感信息(如密码)。
  5. 算法选择
    • HS256 适用于简单场景,RS256 更安全(非对称加密)。

五、使用技巧

5.1 在 Payload 中添加自定义声明

Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("email", user.getEmail());
String token = jwtUtil.generateToken(claims, username);

5.2 提取 Token 中的自定义声明

String userId = (String) jwtUtil.extractClaim(token, claims -> claims.get("userId"));

5.3 使用 Refresh Token(进阶)

  • 实现一个 /refresh-token 接口,使用长期有效的 Refresh Token 换取新的 Access Token。
  • Refresh Token 应存储在安全位置(如 HTTP Only Cookie)。

六、最佳实践

  1. HTTPS 强制:所有 API 必须通过 HTTPS 访问。
  2. 最小权限:Token 中只包含必要权限。
  3. 审计日志:记录 Token 生成、使用和失效事件。
  4. Token 黑名单:实现短期黑名单(如 Redis)应对 Token 泄露。
  5. 版本控制:在 JWT Payload 中加入版本号,便于未来升级。

七、性能优化

  1. 签名算法
    • HS256 计算快,适合高并发。
    • 避免使用计算密集型算法(如 PBKDF2)。
  2. Token 大小
    • 减少 Payload 中的数据量,避免影响网络传输。
  3. 缓存
    • 虽然无状态,但可缓存用户信息(如 Redis)减少数据库查询。
  4. 异步验证
    • 对于极高并发,可考虑异步验证 Token(需谨慎处理线程安全)。

总结

通过以上步骤,你已成功在 Spring Boot 中集成了 JWT 无状态认证。该方案适合现代 Web 应用和微服务架构。

下一步建议

  • 实现 Refresh Token 机制。
  • 集成 Spring Security OAuth2 Resource Server。
  • 学习 JWT 的 JWE(加密)和 JWS(签名)高级用法。
  • 考虑使用 spring-security-oauth2-resource-server 简化 JWT 验证。

本文基于 Spring Boot 3.x + Spring Security 6.x + JJWT 0.11+ 编写。