Skip to content

JWT实现

JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。在Spring Security中,JWT可以用于身份验证和授权,特别适合用于RESTful API和微服务架构。

JWT 基本概念

JWT 的结构

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

  1. Header(头部)

    • 通常包含两部分信息:令牌类型(即JWT)和所使用的签名算法
    • 例如:{ "alg": "HS256", "typ": "JWT" }
  2. Payload(负载)

    • 包含声明(claims)
    • 声明是关于实体(通常是用户)和其他数据的声明
    • 三种类型的声明:注册声明、公共声明和私有声明
  3. Signature(签名)

    • 用于验证消息在传输过程中没有被更改
    • 使用头部指定的算法,对编码后的头部、编码后的负载和一个秘钥进行签名

JWT 工作原理

  1. 用户登录后,服务器创建JWT令牌
  2. 服务器将JWT返回给客户端
  3. 客户端存储JWT(通常在localStorage或Cookie中)
  4. 客户端在后续请求中,将JWT放在Authorization头中
  5. 服务器验证JWT的有效性,并从中提取用户信息

Spring Security 集成 JWT

依赖配置

在Spring Boot项目中添加JWT相关依赖:

xml
<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>

JWT 工具类

创建一个工具类来处理JWT的创建、解析和验证:

java
@Component
public class JwtTokenUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    // 从用户详情生成令牌
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }
    
    // 创建令牌
    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration * 1000);
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }
    
    // 验证令牌
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    
    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    
    // 从令牌中获取过期日期
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    
    // 从令牌中获取声明
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    
    // 解析令牌
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    // 检查令牌是否过期
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
}

自定义JWT认证过滤器

创建一个过滤器来拦截请求并验证JWT:

java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @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;
        
        // JWT Token格式为 "Bearer token",去除Bearer字样,只获取token部分
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                logger.error("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                logger.error("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);
            
            // 如果令牌有效,手动设置Spring Security认证
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 在上下文中设置认证信息
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

配置认证入口点

自定义认证入口点来处理未认证的请求:

java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("message", "Unauthorized access");
        errorDetails.put("error", authException.getMessage());
        
        PrintWriter writer = response.getWriter();
        writer.println(mapper.writeValueAsString(errorDetails));
    }
}

配置Spring Security

配置Spring Security以使用JWT:

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Autowired
    private UserDetailsService jwtUserDetailsService;
    
    @Autowired
    private JwtAuthenticationFilter jwtRequestFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

认证控制器

创建控制器处理用户认证和令牌生成:

java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @PostMapping("/login")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
        
        authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
        
        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());
        
        final String token = jwtTokenUtil.generateToken(userDetails);
        
        return ResponseEntity.ok(new JwtResponse(token));
    }
    
    private void authenticate(String username, String password) throws Exception {
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
    }
}

请求和响应模型

JWT认证请求和响应的模型类:

java
public class JwtRequest {
    
    private String username;
    private String password;
    
    // 默认构造函数与getter和setter
    public JwtRequest() {}
    
    public JwtRequest(String username, String password) {
        this.username = username;
        this.password = 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;
    }
}

public class JwtResponse {
    
    private final String token;
    
    public JwtResponse(String token) {
        this.token = token;
    }
    
    public String getToken() {
        return token;
    }
}

配置属性

application.propertiesapplication.yml中添加JWT相关配置:

properties
# JWT配置
jwt.secret=your-secret-key-should-be-very-long-and-secure
jwt.expiration=86400

JWT 最佳实践与安全考虑

安全最佳实践

  1. 使用HTTPS:始终通过HTTPS传输JWT,以防止中间人攻击。

  2. 适当的令牌过期时间:设置较短的令牌有效期,减少被盗用的风险。

  3. 实施刷新令牌:使用刷新令牌机制,避免用户频繁登录。

  4. 安全存储密钥:JWT签名密钥应安全存储,避免硬编码在源代码中。

  5. 不在JWT中存储敏感信息:JWT的payload部分是Base64编码,不是加密,不应存储敏感数据。

刷新令牌机制

实现刷新令牌机制,延长用户会话:

java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    // ...现有代码
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody JwtRefreshRequest refreshRequest) {
        // 验证刷新令牌
        String refreshToken = refreshRequest.getRefreshToken();
        String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
        
        if (username != null && jwtTokenUtil.validateRefreshToken(refreshToken)) {
            final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            final String accessToken = jwtTokenUtil.generateToken(userDetails);
            
            return ResponseEntity.ok(new JwtResponse(accessToken));
        }
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
    }
}

令牌黑名单

实现令牌黑名单,处理失效的令牌:

java
@Service
public class TokenBlacklistService {
    
    private Set<String> blacklist = new ConcurrentHashSet<>();
    
    public void blacklistToken(String token) {
        blacklist.add(token);
    }
    
    public boolean isBlacklisted(String token) {
        return blacklist.contains(token);
    }
    
    // 定时任务,清理过期的令牌
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    public void purgeExpiredTokens() {
        // 实现过期令牌清理逻辑
    }
}

在JWT过滤器中检查黑名单:

java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private TokenBlacklistService blacklistService;
    
    // ...现有代码
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        
        // ...现有代码
        
        if (jwtToken != null && blacklistService.isBlacklisted(jwtToken)) {
            logger.error("JWT Token is blacklisted");
            // 如果令牌在黑名单中,不进行验证
        } else if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 继续处理有效令牌
        }
        
        // ...现有代码
    }
}

JWT 与微服务架构

在微服务中使用JWT

  1. 集中式认证服务:专门的认证服务负责用户认证和JWT签发。

  2. 令牌传播:微服务之间传递JWT,验证用户身份与权限。

  3. 共享密钥管理:所有微服务共享JWT验证密钥或使用非对称密钥(RSA)。

将JWT与API网关集成

在API网关层验证JWT,减轻后端微服务的认证负担:

java
// API网关中的JWT过滤器示例
@Component
public class GatewayJwtFilter extends AbstractGatewayFilterFactory<GatewayJwtFilter.Config> {
    
    private final JwtTokenUtil jwtTokenUtil;
    
    public GatewayJwtFilter(JwtTokenUtil jwtTokenUtil) {
        super(Config.class);
        this.jwtTokenUtil = jwtTokenUtil;
    }
    
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            
            // 从请求中获取JWT令牌
            String token = extractToken(request);
            
            if (token != null && jwtTokenUtil.validateToken(token)) {
                // 令牌有效,将用户信息添加到请求头
                String username = jwtTokenUtil.getUsernameFromToken(token);
                ServerHttpRequest modifiedRequest = request.mutate()
                        .header("X-User-Id", username)
                        .build();
                
                return chain.filter(exchange.mutate().request(modifiedRequest).build());
            }
            
            // 令牌无效,返回未授权响应
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        };
    }
    
    private String extractToken(ServerHttpRequest request) {
        List<String> authHeaders = request.getHeaders().get("Authorization");
        if (authHeaders != null && !authHeaders.isEmpty()) {
            String authHeader = authHeaders.get(0);
            if (authHeader.startsWith("Bearer ")) {
                return authHeader.substring(7);
            }
        }
        return null;
    }
    
    public static class Config {
        // 配置属性
    }
}

JWT 常见问题与解决方案

问题1:令牌过期处理

问题:用户令牌过期但会话仍然活跃时,如何平滑处理?

解决方案

  • 实现无感刷新机制,通过刷新令牌自动获取新令牌
  • 在令牌即将过期前主动刷新
  • 使用滑动过期时间

问题2:令牌撤销

问题:JWT是无状态的,如何在必要时撤销已签发的令牌?

解决方案

  • 使用令牌黑名单
  • 实现短期令牌策略
  • 使用Redis存储令牌有效状态

问题3:令牌大小

问题:JWT包含所有用户信息,可能变得很大。

解决方案

  • 仅在令牌中存储必要信息(如用户ID)
  • 使用压缩技术
  • 考虑使用引用令牌(Reference Token)而非自包含令牌

总结

  1. JWT提供了一种轻量级、无状态的认证机制,非常适合现代Web应用和微服务架构。

  2. 在Spring Security中集成JWT相对简单,主要包括:

    • 创建JWT工具类处理令牌生成和验证
    • 实现JWT过滤器拦截和处理请求
    • 配置Spring Security使用JWT认证
  3. 使用JWT时需注意安全实践:

    • 保护签名密钥
    • 设置合理的过期时间
    • 不在JWT中存储敏感信息
    • 实现令牌刷新和撤销机制
  4. 微服务架构中,JWT通常与API网关结合使用,集中处理认证逻辑。

通过合理使用JWT,可以实现安全、可扩展的认证系统,简化分布式应用的用户认证与授权管理。