本文系统介绍在 Spring Boot 中使用 JWT 的概念、优势、生成与验证、刷新与撤销、工程化配置与安全要点,并配套示例代码与建议参数。遵循偏安全的默认策略与可维护的架构模式。

概念

  • JWT 由三部分组成:Header、Payload、Signature,经 Base64URL 编码后以 header.payload.signature 形式传输。
  • Header 包含签名算法与类型;Payload 包含声明(如 subissaudexpiatnbfjti);Signature 用密钥或私钥对前两部分签名。
  • JWT 通常作为无状态访问令牌,放在 Authorization: Bearer <token> 头中传输。

优势与适用

  • 无状态与水平扩展,减少服务端会话存储压力。
  • 与前后端分离、移动端、微服务网关等场景天然适配。
  • 通过标准声明与自定义声明承载最小必要身份信息。

限制与注意:

  • 无内置撤销机制,需要配合短有效期、刷新令牌、黑名单、令牌轮换等手段。
  • 令牌较长,不适合频繁通过 URL 传递。

令牌模型

  • 访问令牌(Access Token):有效期短,如 15 分钟。
  • 刷新令牌(Refresh Token):有效期长,如 7 天,仅用于换取新的访问令牌。
  • 建议使用 HS256 或 RS256。生产环境优先 RS256 并实施密钥轮换。

工程化配置

# application.yml
security:
  jwt:
    issuer: example-issuer
    access-exp-minutes: 15
    refresh-exp-days: 7
    hs256-secret: ${JWT_SECRET}
// JwtProperties.java
package com.example.security;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "security.jwt")
public class JwtProperties {
    private String issuer;
    private Integer accessExpMinutes;
    private Integer refreshExpDays;
    private String hs256Secret;
}

生成与验证

服务类

// JwtService.java
package com.example.security;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.Claims;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;

import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class JwtService {
    private final JwtProperties props;

    private Key key() {
        byte[] bytes = props.getHs256Secret().getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(bytes);
    }

    public String generateAccessToken(String subject, Map<String, Object> claims) {
        Instant now = Instant.now();
        Instant exp = now.plus(props.getAccessExpMinutes(), ChronoUnit.MINUTES);
        return Jwts.builder()
                .setIssuer(props.getIssuer())
                .setSubject(subject)
                .setIssuedAt(Date.from(now))
                .setExpiration(Date.from(exp))
                .addClaims(claims)
                .signWith(key(), SignatureAlgorithm.HS256)
                .compact();
    }

    public String generateRefreshToken(String subject) {
        Instant now = Instant.now();
        Instant exp = now.plus(props.getRefreshExpDays(), ChronoUnit.DAYS);
        return Jwts.builder()
                .setIssuer(props.getIssuer())
                .setSubject(subject)
                .setIssuedAt(Date.from(now))
                .setExpiration(Date.from(exp))
                .claim("typ", "refresh")
                .signWith(key(), SignatureAlgorithm.HS256)
                .compact();
    }

    public Claims parse(String token) {
        return Jwts.parserBuilder()
                .requireIssuer(props.getIssuer())
                .setSigningKey(key())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public boolean isAccessToken(String token) {
        Claims c = parse(token);
        Object typ = c.get("typ");
        return typ == null || "access".equals(typ);
    }
}

过滤器

// JwtAuthenticationFilter.java
package com.example.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.Claims;
import java.io.IOException;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Claims claims = jwtService.parse(token);
                String username = claims.getSubject();
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    UserDetails user = userDetailsService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                            user, null, user.getAuthorities());
                    auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception ignored) {}
        }
        chain.doFilter(request, response);
    }
}

安全配置

// SecurityConfig.java
package com.example.security;

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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtService jwtService;
    private final org.springframework.security.core.userdetails.UserDetailsService uds;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtService, uds);
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

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

登录与刷新

// AuthController.java
package com.example.auth;

import com.example.security.JwtService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthenticationManager am;
    private final JwtService jwt;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginReq req) {
        Authentication auth = am.authenticate(new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword()));
        String access = jwt.generateAccessToken(req.getUsername(), Map.of("roles", "user"));
        String refresh = jwt.generateRefreshToken(req.getUsername());
        return ResponseEntity.ok(Map.of("accessToken", access, "refreshToken", refresh));
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refresh(@RequestBody TokenReq req) {
        String subject = jwt.parse(req.getRefreshToken()).getSubject();
        String access = jwt.generateAccessToken(subject, Map.of("roles", "user"));
        return ResponseEntity.ok(Map.of("accessToken", access));
    }

    @Data
    public static class LoginReq { private String username; private String password; }
    @Data
    public static class TokenReq { private String refreshToken; }
}

验证要点与安全策略

  • 验证签名、发行者、受众、过期时间、未生效时间、jti 等关键声明。
  • 使用短期访问令牌与长效刷新令牌,搭配令牌轮换。
  • 刷新令牌建议存放在 HttpOnly、Secure、SameSite 的 Cookie;访问令牌放在请求头。
  • 服务器端维护刷新令牌的 jti 或哈希值,支持撤销与黑名单。
  • 控制声明最小化,只放必要信息;避免敏感数据出现在负载中。
  • 限制跨域与 CSRF 风险,接口启用 CORS 严格白名单与方法限定。
  • 生产环境优先使用 RS256,并规划密钥轮换与 JWKS 发布。
  • 记录失败尝试与异常,配合限流与风控。

常见问题与建议参数

  • exp 与服务器时钟差建议引入微小容忍窗口。
  • 秘钥管理通过环境变量或外部配置中心,避免硬编码。
  • Access Token 15 分钟,Refresh Token 7–30 天,根据安全等级调整。
  • 在网关或 API 层统一验证与鉴权,减少重复逻辑。

总结

  • 以无状态访问令牌 + 刷新令牌的双令牌模型为基础,配合短期有效、声明最小化与统一验证策略。
  • 在 Spring Security 中通过自定义过滤器与配置链实现无状态鉴权,与登录与刷新接口协同。
  • 强化密钥与令牌管理、RS256 与轮换、黑名单与 Cookie 策略以提升整体安全性与可维护性。