3. 令牌技术 (JWT)
CleanShot 2025-10-20 at 17.30.53@2x.png

优点:

  • 支持PC端、移动端
  • 解决集群环境下的认证问题
  • 减轻服务器端存储压力

缺点:需要自己实现

一、JWT 基础概念

1.1 什么是 JWT?

JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息的紧凑且自包含的方式。

1.2 JWT 的结构

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header.Payload.Signature
CleanShot 2025-10-20 at 17.40.19@2x.png

1.2.1 Header (头部)

{
  "alg": "HS256",    // 签名算法
  "typ": "JWT"       // Token 类型
}

1.2.2 Payload (载荷)

{
  "sub": "1234567890",           // Subject (用户ID)
  "name": "John Doe",            // 自定义声明
  "iat": 1516239022,             // Issued At (签发时间)
  "exp": 1516242622,             // Expiration Time (过期时间)
  "iss": "my-app",               // Issuer (签发者)
  "aud": "my-app-users"          // Audience (接收者)
}

标准声明 (Registered Claims):

  • iss (issuer):签发者
  • sub (subject):主题(通常是用户ID)
  • aud (audience):接收者
  • exp (expiration time):过期时间
  • nbf (not before):生效时间
  • iat (issued at):签发时间
  • jti (JWT ID):唯一标识符

1.2.3 Signature (签名)

防止Token被篡改、确保安全性。将header、payload融入,并加入指定秘钥,通过指定签名算法计算而来。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

1.3 JWT 工作流程

┌─────────┐                           ┌─────────┐
│         │  1. 登录请求               │         │
│  客户端  │ ────────────────────────> │  服务器  │
│         │                           │         │
│         │  2. 验证成功,返回 JWT      │         │
│         │ <──────────────────────── │         │
│         │                           │         │
│         │  3. 携带 JWT 请求资源      │         │
│         │ ────────────────────────> │         │
│         │     Authorization:        │         │
│         │     Bearer <token>        │         │
│         │                           │         │
│         │  4. 验证 JWT,返回资源     │         │
│         │ <──────────────────────── │         │
└─────────┘                           └─────────┘

二、Java JWT 主流库对比

库名称特点推荐度GitHub Stars
jjwt简单易用,功能完善⭐⭐⭐⭐⭐9k+
java-jwtAuth0 官方库⭐⭐⭐⭐5k+
nimbus-jose-jwt功能强大,支持 JWE⭐⭐⭐⭐2k+
jose4j轻量级⭐⭐⭐300+

本文主要使用 jjwt 进行讲解


三、Spring Boot 集成 JWT 实战

3.1 项目依赖配置

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  
    <!-- Spring Security (可选,用于权限控制) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  
    <!-- JJWT -->
    <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>
  
    <!-- Lombok (简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3.2 配置文件

# application.yml
jwt:
  secret: mySecretKeyForJWTTokenGenerationAndValidation123456789
  expiration: 86400000  # 24小时 (毫秒)
  refresh-expiration: 604800000  # 7天 (毫秒)
  issuer: my-application
  header: Authorization
  prefix: Bearer 

3.3 JWT 工具类(核心)

package com.example.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Slf4j
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    @Value("${jwt.refresh-expiration}")
    private Long refreshExpiration;

    @Value("${jwt.issuer}")
    private String issuer;

    /**
     * 生成密钥
     */
    private SecretKey getSigningKey() {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 生成 Access Token
     */
    public String generateAccessToken(String userId, Map<String, Object> claims) {
        return createToken(userId, claims, expiration);
    }

    /**
     * 生成 Refresh Token
     */
    public String generateRefreshToken(String userId) {
        return createToken(userId, new HashMap<>(), refreshExpiration);
    }

    /**
     * 创建 Token
     */
    private String createToken(String subject, Map<String, Object> claims, Long expiration) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setClaims(claims)                    // 自定义声明
                .setSubject(subject)                  // 主题 (用户ID)
                .setIssuer(issuer)                    // 签发者
                .setIssuedAt(now)                     // 签发时间
                .setExpiration(expiryDate)            // 过期时间
                .signWith(getSigningKey())            // 签名
                .compact();
    }

    /**
     * 从 Token 中提取用户ID
     */
    public String getUserIdFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

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

    /**
     * 从 Token 中提取指定声明
     */
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    /**
     * 从 Token 中提取所有声明
     */
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 验证 Token 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (ExpiredJwtException e) {
            return true;
        }
    }

    /**
     * 验证 Token
     */
    public Boolean validateToken(String token, String userId) {
        try {
            final String tokenUserId = getUserIdFromToken(token);
            return (tokenUserId.equals(userId) && !isTokenExpired(token));
        } catch (SignatureException e) {
            log.error("Invalid JWT signature: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;
    }

    /**
     * 刷新 Token
     */
    public String refreshToken(String token) {
        try {
            final Claims claims = getAllClaimsFromToken(token);
            claims.setIssuedAt(new Date());
            return createToken(claims.getSubject(), claims, expiration);
        } catch (Exception e) {
            log.error("Could not refresh token: {}", e.getMessage());
            return null;
        }
    }
}

3.4 实体类

package com.example.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String role;
}
package com.example.dto;

import lombok.Data;

@Data
public class LoginRequest {
    private String username;
    private String password;
}
package com.example.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AuthResponse {
    private String accessToken;
    private String refreshToken;
    private String tokenType = "Bearer";
    private Long expiresIn;
  
    public AuthResponse(String accessToken, String refreshToken, Long expiresIn) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.expiresIn = expiresIn;
    }
}

3.5 认证控制器

package com.example.controller;

import com.example.dto.AuthResponse;
import com.example.dto.LoginRequest;
import com.example.entity.User;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final PasswordEncoder passwordEncoder;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        // 1. 验证用户
        User user = userService.findByUsername(request.getUsername());
        if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }

        // 2. 生成 Token
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", user.getRole());
        claims.put("email", user.getEmail());

        String accessToken = jwtUtil.generateAccessToken(user.getId().toString(), claims);
        String refreshToken = jwtUtil.generateRefreshToken(user.getId().toString());

        // 3. 返回 Token
        AuthResponse response = new AuthResponse(
                accessToken,
                refreshToken,
                86400L  // 24小时
        );

        return ResponseEntity.ok(response);
    }

    /**
     * 刷新 Token
     */
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String refreshToken) {
        try {
            // 移除 "Bearer " 前缀
            String token = refreshToken.substring(7);

            // 验证 Refresh Token
            String userId = jwtUtil.getUserIdFromToken(token);
            if (jwtUtil.isTokenExpired(token)) {
                return ResponseEntity.status(401).body("Refresh Token 已过期");
            }

            // 生成新的 Access Token
            User user = userService.findById(Long.parseLong(userId));
            Map<String, Object> claims = new HashMap<>();
            claims.put("role", user.getRole());
            claims.put("email", user.getEmail());

            String newAccessToken = jwtUtil.generateAccessToken(userId, claims);

            AuthResponse response = new AuthResponse(
                    newAccessToken,
                    token,  // 返回原 Refresh Token
                    86400L
            );

            return ResponseEntity.ok(response);
        } catch (Exception e) {
            return ResponseEntity.status(401).body("无效的 Refresh Token");
        }
    }

    /**
     * 登出(可选:将 Token 加入黑名单)
     */
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
        // 实现 Token 黑名单逻辑
        // tokenBlacklistService.addToBlacklist(token);
        return ResponseEntity.ok("登出成功");
    }
}

3.6 JWT 过滤器

package com.example.filter;

import com.example.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            // 1. 从请求头中获取 Token
            String token = getTokenFromRequest(request);

            // 2. 验证 Token
            if (StringUtils.hasText(token)) {
                String userId = jwtUtil.getUserIdFromToken(token);

                if (userId != null && jwtUtil.validateToken(token, userId)) {
                    // 3. 设置认证信息到 SecurityContext
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(
                                    userId,
                                    null,
                                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
                            );

                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (Exception e) {
            log.error("Could not set user authentication in security context", e);
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 从请求头中提取 Token
     */
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3.7 Spring Security 配置

package com.example.config;

import com.example.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
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.EnableGlobalMethodSecurity;
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
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 禁用 CSRF(使用 JWT 不需要)
                .csrf().disable()
              
                // 配置会话管理为无状态
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
              
                .and()
              
                // 配置请求授权
                .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()  // 登录接口允许匿名访问
                .antMatchers("/api/public/**").permitAll() // 公开接口
                .anyRequest().authenticated()              // 其他接口需要认证
              
                .and()
              
                // 添加 JWT 过滤器
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

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

3.8 受保护的资源接口

package com.example.controller;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserController {

    /**
     * 获取当前用户信息
     */
    @GetMapping("/profile")
    public Map<String, Object> getProfile() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String userId = (String) authentication.getPrincipal();

        Map<String, Object> profile = new HashMap<>();
        profile.put("userId", userId);
        profile.put("message", "这是受保护的资源");
      
        return profile;
    }

    /**
     * 仅管理员可访问
     */
    @GetMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')")
    public String getUsers() {
        return "用户列表(仅管理员可见)";
    }

    /**
     * 测试接口
     */
    @GetMapping("/test")
    public String test() {
        return "JWT 认证成功!";
    }
}

四、高级特性实现

4.1 Token 黑名单(实现登出功能)

package com.example.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class TokenBlacklistService {

    private final RedisTemplate<String, String> redisTemplate;
    private static final String BLACKLIST_PREFIX = "jwt:blacklist:";

    /**
     * 将 Token 加入黑名单
     */
    public void addToBlacklist(String token, Long expirationTime) {
        String key = BLACKLIST_PREFIX + token;
        redisTemplate.opsForValue().set(key, "1", expirationTime, TimeUnit.MILLISECONDS);
    }

    /**
     * 检查 Token 是否在黑名单中
     */
    public boolean isBlacklisted(String token) {
        String key = BLACKLIST_PREFIX + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}

修改 JwtAuthenticationFilter:

@Override
protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
) throws ServletException, IOException {
    try {
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token)) {
            // 检查黑名单
            if (tokenBlacklistService.isBlacklisted(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("Token 已失效");
                return;
            }

            String userId = jwtUtil.getUserIdFromToken(token);
            if (userId != null && jwtUtil.validateToken(token, userId)) {
                // 设置认证信息
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userId, null,
                                Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
                        );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
    } catch (Exception e) {
        log.error("Could not set user authentication", e);
    }

    filterChain.doFilter(request, response);
}

4.2 多设备登录管理

package com.example.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class MultiDeviceLoginService {

    private final RedisTemplate<String, String> redisTemplate;
    private static final String USER_TOKENS_PREFIX = "user:tokens:";
    private static final int MAX_DEVICES = 3;  // 最多允许3个设备同时登录

    /**
     * 记录用户的 Token
     */
    public void recordUserToken(String userId, String token, Long expiration) {
        String key = USER_TOKENS_PREFIX + userId;
      
        // 获取当前用户的所有 Token
        Set<String> tokens = redisTemplate.opsForSet().members(key);
      
        // 如果超过最大设备数,移除最早的 Token
        if (tokens != null && tokens.size() >= MAX_DEVICES) {
            String oldestToken = tokens.iterator().next();
            redisTemplate.opsForSet().remove(key, oldestToken);
            // 将旧 Token 加入黑名单
            // tokenBlacklistService.addToBlacklist(oldestToken, expiration);
        }
      
        // 添加新 Token
        redisTemplate.opsForSet().add(key, token);
        redisTemplate.expire(key, expiration, TimeUnit.MILLISECONDS);
    }

    /**
     * 踢出用户的所有设备
     */
    public void kickoutAllDevices(String userId) {
        String key = USER_TOKENS_PREFIX + userId;
        Set<String> tokens = redisTemplate.opsForSet().members(key);
      
        if (tokens != null) {
            for (String token : tokens) {
                // 将所有 Token 加入黑名单
                // tokenBlacklistService.addToBlacklist(token, expiration);
            }
        }
      
        redisTemplate.delete(key);
    }
}

4.3 Token 自动续期

package com.example.filter;

import com.example.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class TokenRefreshFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private static final long REFRESH_THRESHOLD = 30 * 60 * 1000; // 30分钟

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
      
        String token = getTokenFromRequest(request);
      
        if (token != null) {
            try {
                Date expirationDate = jwtUtil.getExpirationDateFromToken(token);
                long timeUntilExpiration = expirationDate.getTime() - System.currentTimeMillis();
              
                // 如果 Token 即将过期(30分钟内),自动刷新
                if (timeUntilExpiration > 0 && timeUntilExpiration < REFRESH_THRESHOLD) {
                    String newToken = jwtUtil.refreshToken(token);
                    response.setHeader("New-Token", newToken);
                }
            } catch (Exception e) {
                // Token 无效,不处理
            }
        }
      
        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

4.4 权限控制

package com.example.util;

import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class PermissionUtil {

    /**
     * 检查用户是否有指定权限
     */
    public boolean hasPermission(Claims claims, String permission) {
        List<String> permissions = claims.get("permissions", List.class);
        return permissions != null && permissions.contains(permission);
    }

    /**
     * 检查用户是否有指定角色
     */
    public boolean hasRole(Claims claims, String role) {
        String userRole = claims.get("role", String.class);
        return role.equals(userRole);
    }
}

在控制器中使用:

@GetMapping("/admin/sensitive-data")
public ResponseEntity<?> getSensitiveData(@RequestHeader("Authorization") String token) {
    String jwtToken = token.substring(7);
    Claims claims = jwtUtil.getAllClaimsFromToken(jwtToken);
  
    if (!permissionUtil.hasRole(claims, "ADMIN")) {
        return ResponseEntity.status(403).body("权限不足");
    }
  
    return ResponseEntity.ok("敏感数据");
}

五、安全最佳实践

5.1 密钥管理

// ❌ 错误:硬编码密钥
private static final String SECRET = "mySecret";

// ✅ 正确:使用环境变量
@Value("${jwt.secret}")
private String secret;

// ✅ 更好:使用密钥管理服务(如 AWS KMS、Azure Key Vault)

5.2 使用强加密算法

// ✅ 推荐使用 HS256 或 RS256
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

// 或使用 RSA 非对称加密
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);

5.3 设置合理的过期时间

// Access Token: 15分钟 - 1小时
private static final long ACCESS_TOKEN_EXPIRATION = 15 * 60 * 1000;

// Refresh Token: 7天 - 30天
private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 

5.4 防止 Token 泄露

@Configuration
public class SecurityHeadersConfig {
  
    @Bean
    public FilterRegistrationBean<SecurityHeadersFilter> securityHeadersFilter() {
        FilterRegistrationBean<SecurityHeadersFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new SecurityHeadersFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

public class SecurityHeadersFilter implements Filter {
  
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
      
        // 防止 XSS 攻击
        httpResponse.setHeader("X-Content-Type-Options", "nosniff");
        httpResponse.setHeader("X-Frame-Options", "DENY");
        httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
      
        // 强制 HTTPS
        httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
      
        // CSP 策略
        httpResponse.setHeader("Content-Security-Policy", "default-src 'self'");
      
        chain.doFilter(request, response);
    }
}

5.5 防止暴力破解

package com.example.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class LoginAttemptService {

    private final RedisTemplate<String, Integer> redisTemplate;
    private static final String LOGIN_ATTEMPT_PREFIX = "login:attempt:";
    private static final int MAX_ATTEMPTS = 5;
    private static final long LOCK_TIME = 15; // 15分钟

    /**
     * 记录登录失败
     */
    public void loginFailed(String username) {
        String key = LOGIN_ATTEMPT_PREFIX + username;
        Integer attempts = redisTemplate.opsForValue().get(key);
      
        if (attempts == null) {
            attempts = 0;
        }
      
        attempts++;
        redisTemplate.opsForValue().set(key, attempts, LOCK_TIME, TimeUnit.MINUTES);
    }

    /**
     * 登录成功,清除失败记录
     */
    public void loginSucceeded(String username) {
        String key = LOGIN_ATTEMPT_PREFIX + username;
        redisTemplate.delete(key);
    }

    /**
     * 检查是否被锁定
     */
    public boolean isBlocked(String username) {
        String key = LOGIN_ATTEMPT_PREFIX + username;
        Integer attempts = redisTemplate.opsForValue().get(key);
        return attempts != null && attempts >= MAX_ATTEMPTS;
    }

    /**
     * 获取剩余尝试次数
     */
    public int getRemainingAttempts(String username) {
        String key = LOGIN_ATTEMPT_PREFIX + username;
        Integer attempts = redisTemplate.opsForValue().get(key);
        if (attempts == null) {
            return MAX_ATTEMPTS;
        }
        return Math.max(0, MAX_ATTEMPTS - attempts);
    }
}

在登录接口中使用:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
    // 1. 检查是否被锁定
    if (loginAttemptService.isBlocked(request.getUsername())) {
        return ResponseEntity.status(429)
                .body("登录失败次数过多,账号已被锁定15分钟");
    }

    // 2. 验证用户
    User user = userService.findByUsername(request.getUsername());
    if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
        loginAttemptService.loginFailed(request.getUsername());
        int remaining = loginAttemptService.getRemainingAttempts(request.getUsername());
        return ResponseEntity.status(401)
                .body("用户名或密码错误,剩余尝试次数: " + remaining);
    }

    // 3. 登录成功
    loginAttemptService.loginSucceeded(request.getUsername());

    // 4. 生成 Token
    Map<String, Object> claims = new HashMap<>();
    claims.put("role", user.getRole());
    claims.put("email", user.getEmail());

    String accessToken = jwtUtil.generateAccessToken(user.getId().toString(), claims);
    String refreshToken = jwtUtil.generateRefreshToken(user.getId().toString());

    return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken, 86400L));
}

六、完整的双 Token 机制实现

6.1 Token 存储策略

package com.example.service;

import com.example.entity.RefreshToken;
import com.example.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository;
    private static final long REFRESH_TOKEN_DURATION = 7 * 24 * 60 * 60 * 1000L; // 7天

    /**
     * 创建 Refresh Token
     */
    @Transactional
    public RefreshToken createRefreshToken(Long userId) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUserId(userId);
        refreshToken.setToken(UUID.randomUUID().toString());
        refreshToken.setExpiryDate(Instant.now().plusMillis(REFRESH_TOKEN_DURATION));
      
        return refreshTokenRepository.save(refreshToken);
    }

    /**
     * 验证 Refresh Token
     */
    public Optional<RefreshToken> findByToken(String token) {
        return refreshTokenRepository.findByToken(token);
    }

    /**
     * 验证 Token 是否过期
     */
    public RefreshToken verifyExpiration(RefreshToken token) {
        if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
            refreshTokenRepository.delete(token);
            throw new RuntimeException("Refresh token 已过期,请重新登录");
        }
        return token;
    }

    /**
     * 删除用户的所有 Refresh Token
     */
    @Transactional
    public void deleteByUserId(Long userId) {
        refreshTokenRepository.deleteByUserId(userId);
    }
}

6.2 Refresh Token 实体

package com.example.entity;

import lombok.Data;

import javax.persistence.*;
import java.time.Instant;

@Entity
@Table(name = "refresh_tokens")
@Data
public class RefreshToken {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String token;

    @Column(nullable = false)
    private Long userId;

    @Column(nullable = false)
    private Instant expiryDate;

    @Column(name = "created_at")
    private Instant createdAt = Instant.now();
}

6.3 Repository

package com.example.repository;

import com.example.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
  
    Optional<RefreshToken> findByToken(String token);
  
    void deleteByUserId(Long userId);
}

6.4 完整的认证流程

package com.example.controller;

import com.example.dto.AuthResponse;
import com.example.dto.LoginRequest;
import com.example.dto.RefreshTokenRequest;
import com.example.entity.RefreshToken;
import com.example.entity.User;
import com.example.service.RefreshTokenService;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final UserService userService;
    private final RefreshTokenService refreshTokenService;
    private final JwtUtil jwtUtil;
    private final PasswordEncoder passwordEncoder;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        // 1. 验证用户
        User user = userService.findByUsername(request.getUsername());
        if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }

        // 2. 生成 Access Token
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", user.getRole());
        claims.put("email", user.getEmail());
        claims.put("username", user.getUsername());

        String accessToken = jwtUtil.generateAccessToken(user.getId().toString(), claims);

        // 3. 生成并保存 Refresh Token
        RefreshToken refreshToken = refreshTokenService.createRefreshToken(user.getId());

        // 4. 返回响应
        AuthResponse response = new AuthResponse(
                accessToken,
                refreshToken.getToken(),
                86400L  // 24小时
        );

        return ResponseEntity.ok(response);
    }

    /**
     * 刷新 Access Token
     */
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
        String requestRefreshToken = request.getRefreshToken();

        return refreshTokenService.findByToken(requestRefreshToken)
                .map(refreshTokenService::verifyExpiration)
                .map(RefreshToken::getUserId)
                .map(userId -> {
                    User user = userService.findById(userId);
                  
                    // 生成新的 Access Token
                    Map<String, Object> claims = new HashMap<>();
                    claims.put("role", user.getRole());
                    claims.put("email", user.getEmail());
                    claims.put("username", user.getUsername());

                    String newAccessToken = jwtUtil.generateAccessToken(
                            userId.toString(), 
                            claims
                    );

                    AuthResponse response = new AuthResponse(
                            newAccessToken,
                            requestRefreshToken,
                            86400L
                    );

                    return ResponseEntity.ok(response);
                })
                .orElseThrow(() -> new RuntimeException("Refresh token 不存在"));
    }

    /**
     * 登出
     */
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
        try {
            String jwtToken = token.substring(7);
            String userId = jwtUtil.getUserIdFromToken(jwtToken);
          
            // 删除用户的所有 Refresh Token
            refreshTokenService.deleteByUserId(Long.parseLong(userId));
          
            // 将当前 Access Token 加入黑名单
            // tokenBlacklistService.addToBlacklist(jwtToken, expirationTime);
          
            return ResponseEntity.ok("登出成功");
        } catch (Exception e) {
            return ResponseEntity.status(400).body("登出失败");
        }
    }

    /**
     * 登出所有设备
     */
    @PostMapping("/logout-all")
    public ResponseEntity<?> logoutAll(@RequestHeader("Authorization") String token) {
        try {
            String jwtToken = token.substring(7);
            String userId = jwtUtil.getUserIdFromToken(jwtToken);
          
            // 删除用户的所有 Refresh Token
            refreshTokenService.deleteByUserId(Long.parseLong(userId));
          
            return ResponseEntity.ok("已登出所有设备");
        } catch (Exception e) {
            return ResponseEntity.status(400).body("操作失败");
        }
    }
}

七、异常处理

7.1 自定义异常

package com.example.exception;

public class JwtException extends RuntimeException {
    public JwtException(String message) {
        super(message);
    }
}

public class TokenExpiredException extends JwtException {
    public TokenExpiredException(String message) {
        super(message);
    }
}

public class InvalidTokenException extends JwtException {
    public InvalidTokenException(String message) {
        super(message);
    }
}

7.2 全局异常处理器

package com.example.exception;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * JWT Token 过期
     */
    @ExceptionHandler(ExpiredJwtException.class)
    public ResponseEntity<Map<String, Object>> handleExpiredJwtException(ExpiredJwtException e) {
        log.error("JWT Token 已过期: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.UNAUTHORIZED, "TOKEN_EXPIRED", "Token 已过期,请刷新");
    }

    /**
     * JWT Token 格式错误
     */
    @ExceptionHandler(MalformedJwtException.class)
    public ResponseEntity<Map<String, Object>> handleMalformedJwtException(MalformedJwtException e) {
        log.error("JWT Token 格式错误: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.UNAUTHORIZED, "INVALID_TOKEN", "Token 格式错误");
    }

    /**
     * JWT 签名验证失败
     */
    @ExceptionHandler(SignatureException.class)
    public ResponseEntity<Map<String, Object>> handleSignatureException(SignatureException e) {
        log.error("JWT 签名验证失败: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.UNAUTHORIZED, "INVALID_SIGNATURE", "Token 签名无效");
    }

    /**
     * 认证失败
     */
    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<Map<String, Object>> handleBadCredentialsException(BadCredentialsException e) {
        log.error("认证失败: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.UNAUTHORIZED, "BAD_CREDENTIALS", "用户名或密码错误");
    }

    /**
     * 权限不足
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<Map<String, Object>> handleAccessDeniedException(AccessDeniedException e) {
        log.error("权限不足: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.FORBIDDEN, "ACCESS_DENIED", "权限不足");
    }

    /**
     * 自定义 JWT 异常
     */
    @ExceptionHandler(JwtException.class)
    public ResponseEntity<Map<String, Object>> handleJwtException(JwtException e) {
        log.error("JWT 异常: {}", e.getMessage());
        return buildErrorResponse(HttpStatus.UNAUTHORIZED, "JWT_ERROR", e.getMessage());
    }

    /**
     * 通用异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception e) {
        log.error("系统异常: ", e);
        return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "系统内部错误");
    }

    /**
     * 构建错误响应
     */
    private ResponseEntity<Map<String, Object>> buildErrorResponse(
            HttpStatus status, 
            String code, 
            String message
    ) {
        Map<String, Object> error = new HashMap<>();
        error.put("status", status.value());
        error.put("code", code);
        error.put("message", message);
        error.put("timestamp", System.currentTimeMillis());
      
        return ResponseEntity.status(status).body(error);
    }
}

八、测试

8.1 单元测试

package com.example.util;

import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

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

import static org.junit.jupiter.api.Assertions.*;

class JwtUtilTest {

    private JwtUtil jwtUtil;
    private static final String TEST_SECRET = "testSecretKeyForJWTTokenGenerationAndValidation123456789";
    private static final Long TEST_EXPIRATION = 3600000L; // 1小时

    @BeforeEach
    void setUp() {
        jwtUtil = new JwtUtil();
        ReflectionTestUtils.setField(jwtUtil, "secret", TEST_SECRET);
        ReflectionTestUtils.setField(jwtUtil, "expiration", TEST_EXPIRATION);
        ReflectionTestUtils.setField(jwtUtil, "issuer", "test-app");
    }

    @Test
    void testGenerateToken() {
        // Given
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", "USER");
        claims.put("email", "test@example.com");

        // When
        String token = jwtUtil.generateAccessToken(userId, claims);

        // Then
        assertNotNull(token);
        assertTrue(token.split("\\.").length == 3); // JWT 应该有3部分
    }

    @Test
    void testGetUserIdFromToken() {
        // Given
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        String token = jwtUtil.generateAccessToken(userId, claims);

        // When
        String extractedUserId = jwtUtil.getUserIdFromToken(token);

        // Then
        assertEquals(userId, extractedUserId);
    }

    @Test
    void testValidateToken() {
        // Given
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        String token = jwtUtil.generateAccessToken(userId, claims);

        // When
        Boolean isValid = jwtUtil.validateToken(token, userId);

        // Then
        assertTrue(isValid);
    }

    @Test
    void testValidateTokenWithWrongUserId() {
        // Given
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        String token = jwtUtil.generateAccessToken(userId, claims);

        // When
        Boolean isValid = jwtUtil.validateToken(token, "99999");

        // Then
        assertFalse(isValid);
    }

    @Test
    void testGetClaimsFromToken() {
        // Given
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", "ADMIN");
        claims.put("email", "admin@example.com");
        String token = jwtUtil.generateAccessToken(userId, claims);

        // When
        Claims extractedClaims = jwtUtil.getAllClaimsFromToken(token);

        // Then
        assertEquals("ADMIN", extractedClaims.get("role"));
        assertEquals("admin@example.com", extractedClaims.get("email"));
        assertEquals(userId, extractedClaims.getSubject());
    }

    @Test
    void testTokenExpiration() throws InterruptedException {
        // Given
        ReflectionTestUtils.setField(jwtUtil, "expiration", 1000L); // 1秒过期
        String userId = "12345";
        Map<String, Object> claims = new HashMap<>();
        String token = jwtUtil.generateAccessToken(userId, claims);

        // When
        Thread.sleep(1500); // 等待1.5秒
        Boolean isExpired = jwtUtil.isTokenExpired(token);

        // Then
        assertTrue(isExpired);
    }
}

8.2 集成测试

package com.example.controller;

import com.example.dto.LoginRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void testLoginSuccess() throws Exception {
        // Given
        LoginRequest request = new LoginRequest();
        request.setUsername("testuser");
        request.setPassword("password123");

        // When & Then
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.accessToken").exists())
                .andExpect(jsonPath("$.refreshToken").exists())
                .andExpect(jsonPath("$.tokenType").value("Bearer"));
    }

    @Test
    void testLoginFailure() throws Exception {
        // Given
        LoginRequest request = new LoginRequest();
        request.setUsername("testuser");
        request.setPassword("wrongpassword");

        // When & Then
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void testAccessProtectedResourceWithValidToken() throws Exception {
        // Given - 先登录获取 Token
        LoginRequest request = new LoginRequest();
        request.setUsername("testuser");
        request.setPassword("password123");

        MvcResult loginResult = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andReturn();

        String responseBody = loginResult.getResponse().getContentAsString();
        String accessToken = objectMapper.readTree(responseBody).get("accessToken").asText();

        // When & Then - 使用 Token 访问受保护资源
        mockMvc.perform(get("/api/profile")
                        .header("Authorization", "Bearer " + accessToken))
                .andExpect(status().isOk());
    }

    @Test
    void testAccessProtectedResourceWithoutToken() throws Exception {
        // When & Then
        mockMvc.perform(get("/api/profile"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void testRefreshToken() throws Exception {
        // Given - 先登录获取 Token
        LoginRequest request = new LoginRequest();
        request.setUsername("testuser");
        request.setPassword("password123");

        MvcResult loginResult = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andReturn();

        String responseBody = loginResult.getResponse().getContentAsString();
        String refreshToken = objectMapper.readTree(responseBody).get("refreshToken").asText();

        // When & Then - 刷新 Token
        mockMvc.perform(post("/api/auth/refresh")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"refreshToken\":\"" + refreshToken + "\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.accessToken").exists());
    }
}

九、性能优化

9.1 Token 缓存

package com.example.service;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class TokenCacheService {

    private final JwtUtil jwtUtil;

    // 本地缓存,避免每次都解析 Token
    private final Cache<String, Claims> tokenCache = Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .maximumSize(10000)
            .build();

    /**
     * 获取 Token 的 Claims(带缓存)
     */
    public Claims getClaims(String token) {
        return tokenCache.get(token, key -> jwtUtil.getAllClaimsFromToken(key));
    }

    /**
     * 清除缓存
     */
    public void invalidate(String token) {
        tokenCache.invalidate(token);
    }
}

9.2 异步日志记录

package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class AuditLogService {

    @Async
    public void logLoginAttempt(String username, String ip, boolean success) {
        // 异步记录登录日志
        log.info("Login attempt - Username: {}, IP: {}, Success: {}", 
                username, ip, success);
      
        // 保存到数据库
        // auditLogRepository.save(new AuditLog(...));
    }

    @Async
    public void logTokenRefresh(String userId, String ip) {
        log.info("Token refresh - UserId: {}, IP: {}", userId, ip);
    }
}

十、JWT vs Session 对比总结

特性JWTSession
存储位置客户端服务器端
扩展性⭐⭐⭐⭐⭐ 无状态,易扩展⭐⭐⭐ 需要共享存储
性能⭐⭐⭐⭐ 无需查询⭐⭐⭐ 需要查询存储
安全性⭐⭐⭐ 无法主动失效⭐⭐⭐⭐ 可主动销毁
跨域支持⭐⭐⭐⭐⭐ 天然支持⭐⭐⭐ 需要配置
移动端支持⭐⭐⭐⭐⭐ 完美支持⭐⭐⭐ 需要特殊处理
Token 大小⭐⭐⭐ 较大⭐⭐⭐⭐ 仅 Session ID
实现复杂度⭐⭐⭐⭐ 相对简单⭐⭐⭐ 需要存储管理

十一、最佳实践总结

11.1 推荐的 JWT 配置

jwt:
  # 使用强密钥(至少256位)
  secret: ${JWT_SECRET:your-256-bit-secret-key-here}

  # Access Token 短期有效(15分钟 - 1小时)
  access-token-expiration: 900000  # 15分钟

  # Refresh Token 长期有效(7天 - 30天)
  refresh-token-expiration: 604800000  # 7天

  # 签发者
  issuer: ${spring.application.name}

  # 请求头名称
  header: Authorization

  # Token 前缀
  prefix: Bearer 

11.2 安全检查清单

  • ✅ 使用 HTTPS 传输
  • ✅ 密钥存储在环境变量中
  • ✅ 设置合理的过期时间
  • ✅ 实现 Token 刷新机制
  • ✅ 添加 Token 黑名单功能
  • ✅ 实现登录失败次数限制
  • ✅ 记录审计日志
  • ✅ 使用强加密算法(HS256/RS256)
  • ✅ 验证 Token 签名
  • ✅ 检查 Token 过期时间
  • ✅ 防止 XSS 和 CSRF 攻击

11.3 常见问题及解决方案

问题1:Token 过大导致请求头超限

解决方案:

// 减少 Payload 中的数据
Map<String, Object> claims = new HashMap<>();
claims.put("role", user.getRole());  // 只存储必要信息
// 不要存储大量数据

问题2:Token 无法主动失效

解决方案:

// 实现 Token 黑名单 + Refresh Token 机制
// 使用短期 Access Token + 长期 Refresh
Token
// 登出时将 Refresh Token 从数据库删除

问题3:分布式环境下的 Token 验证性能问题

解决方案:

// 使用 Redis 缓存已验证的 Token
@Service
@RequiredArgsConstructor
public class TokenValidationCacheService {
  
    private final RedisTemplate<String, String> redisTemplate;
    private static final String VALIDATED_TOKEN_PREFIX = "validated:token:";
    private static final long CACHE_DURATION = 5 * 60; // 5分钟

    /**
     * 缓存已验证的 Token
     */
    public void cacheValidatedToken(String token, String userId) {
        String key = VALIDATED_TOKEN_PREFIX + token;
        redisTemplate.opsForValue().set(key, userId, CACHE_DURATION, TimeUnit.SECONDS);
    }

    /**
     * 检查 Token 是否已验证
     */
    public String getValidatedUserId(String token) {
        String key = VALIDATED_TOKEN_PREFIX + token;
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 清除缓存
     */
    public void invalidate(String token) {
        String key = VALIDATED_TOKEN_PREFIX + token;
        redisTemplate.delete(key);
    }
}

优化后的过滤器:

@Override
protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
) throws ServletException, IOException {
    try {
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token)) {
            // 1. 先检查缓存
            String cachedUserId = tokenValidationCacheService.getValidatedUserId(token);
          
            if (cachedUserId != null) {
                // 缓存命中,直接使用
                setAuthentication(cachedUserId);
            } else {
                // 2. 缓存未命中,验证 Token
                String userId = jwtUtil.getUserIdFromToken(token);
              
                if (userId != null && jwtUtil.validateToken(token, userId)) {
                    // 3. 验证成功,缓存结果
                    tokenValidationCacheService.cacheValidatedToken(token, userId);
                    setAuthentication(userId);
                }
            }
        }
    } catch (Exception e) {
        log.error("Token validation error", e);
    }

    filterChain.doFilter(request, response);
}

private void setAuthentication(String userId) {
    UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(
                    userId, null,
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
            );
    SecurityContextHolder.getContext().setAuthentication(authentication);
}

问题4:前端如何安全存储 Token

不推荐方案:

// ❌ 不要存储在 localStorage(易受 XSS 攻击)
localStorage.setItem('token', token);

// ❌ 不要存储在 sessionStorage
sessionStorage.setItem('token', token);

推荐方案:

// ✅ 方案1:存储在内存中(最安全,但刷新页面会丢失)
let accessToken = '';

function setToken(token) {
    accessToken = token;
}

function getToken() {
    return accessToken;
}

// ✅ 方案2:HttpOnly Cookie(需要后端配合)
// 后端将 Token 设置在 HttpOnly Cookie 中
// 前端无需手动管理,浏览器自动携带

// ✅ 方案3:混合方案(推荐)
// Access Token 存储在内存
// Refresh Token 存储在 HttpOnly Cookie
class TokenManager {
    constructor() {
        this.accessToken = null;
    }

    setAccessToken(token) {
        this.accessToken = token;
    }

    getAccessToken() {
        return this.accessToken;
    }

    async refreshAccessToken() {
        // Refresh Token 在 Cookie 中,自动携带
        const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            credentials: 'include'
        });
        const data = await response.json();
        this.setAccessToken(data.accessToken);
        return data.accessToken;
    }

    clearTokens() {
        this.accessToken = null;
    }
}

const tokenManager = new TokenManager();

十二、高级场景实现

12.1 多租户系统的 JWT 实现

package com.example.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class MultiTenantJwtUtil {

    private final Map<String, SecretKey> tenantSecrets = new HashMap<>();

    /**
     * 为每个租户生成独立的密钥
     */
    public SecretKey getTenantSecret(String tenantId) {
        return tenantSecrets.computeIfAbsent(tenantId, 
            id -> Keys.secretKeyFor(SignatureAlgorithm.HS256));
    }

    /**
     * 生成包含租户信息的 Token
     */
    public String generateToken(String userId, String tenantId, Map<String, Object> claims) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + 86400000);

        claims.put("tenantId", tenantId);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userId)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(getTenantSecret(tenantId))
                .compact();
    }

    /**
     * 验证 Token 并提取租户信息
     */
    public Claims validateToken(String token) {
        // 先解析 Token 获取租户ID(不验证签名)
        Claims claims = Jwts.parserBuilder()
                .build()
                .parseClaimsJwt(token.substring(0, token.lastIndexOf('.') + 1))
                .getBody();

        String tenantId = claims.get("tenantId", String.class);

        // 使用租户的密钥验证签名
        return Jwts.parserBuilder()
                .setSigningKey(getTenantSecret(tenantId))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

12.2 基于角色的动态权限控制

package com.example.security;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class DynamicPermissionResolver {

    /**
     * 从 Token 中解析权限
     */
    public List<GrantedAuthority> resolvePermissions(Claims claims) {
        List<GrantedAuthority> authorities = new ArrayList<>();

        // 1. 添加角色
        String role = claims.get("role", String.class);
        if (role != null) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }

        // 2. 添加细粒度权限
        @SuppressWarnings("unchecked")
        List<String> permissions = claims.get("permissions", List.class);
        if (permissions != null) {
            authorities.addAll(permissions.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList()));
        }

        return authorities;
    }
}

生成包含权限的 Token:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
    User user = userService.findByUsername(request.getUsername());
  
    // 获取用户的角色和权限
    List<String> permissions = permissionService.getUserPermissions(user.getId());
  
    Map<String, Object> claims = new HashMap<>();
    claims.put("role", user.getRole());
    claims.put("permissions", permissions);  // ["user:read", "user:write", "order:read"]
  
    String token = jwtUtil.generateAccessToken(user.getId().toString(), claims);
  
    return ResponseEntity.ok(new AuthResponse(token, null, 86400L));
}

在控制器中使用:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    @PreAuthorize("hasAuthority('user:read')")
    public List<User> getUsers() {
        return userService.findAll();
    }

    @PostMapping
    @PreAuthorize("hasAuthority('user:write')")
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('user:delete')")
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

12.3 Token 降级策略

package com.example.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class TokenDegradationService {

    private final JwtUtil jwtUtil;
    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 生成降级 Token(功能受限)
     */
    public String generateDegradedToken(String userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("degraded", true);
        claims.put("permissions", Arrays.asList("read:basic"));  // 仅基础读权限
      
        return jwtUtil.generateAccessToken(userId, claims);
    }

    /**
     * 检查是否为降级 Token
     */
    public boolean isDegraded(String token) {
        try {
            Claims claims = jwtUtil.getAllClaimsFromToken(token);
            return Boolean.TRUE.equals(claims.get("degraded", Boolean.class));
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 升级 Token
     */
    public String upgradeToken(String degradedToken, String password) {
        // 验证密码后升级为完整权限 Token
        Claims claims = jwtUtil.getAllClaimsFromToken(degradedToken);
        String userId = claims.getSubject();
      
        // 验证密码
        User user = userService.findById(Long.parseLong(userId));
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
      
        // 生成完整权限 Token
        Map<String, Object> newClaims = new HashMap<>();
        newClaims.put("role", user.getRole());
        newClaims.put("permissions", permissionService.getUserPermissions(user.getId()));
      
        return jwtUtil.generateAccessToken(userId, newClaims);
    }
}

12.4 Token 版本控制

package com.example.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class TokenVersionService {

    private final RedisTemplate<String, Integer> redisTemplate;
    private static final String TOKEN_VERSION_PREFIX = "token:version:";

    /**
     * 获取用户的 Token 版本
     */
    public int getUserTokenVersion(String userId) {
        String key = TOKEN_VERSION_PREFIX + userId;
        Integer version = redisTemplate.opsForValue().get(key);
        return version != null ? version : 0;
    }

    /**
     * 增加用户的 Token 版本(使所有旧 Token 失效)
     */
    public void incrementTokenVersion(String userId) {
        String key = TOKEN_VERSION_PREFIX + userId;
        redisTemplate.opsForValue().increment(key);
    }

    /**
     * 验证 Token 版本
     */
    public boolean validateTokenVersion(String token, String userId) {
        try {
            Claims claims = jwtUtil.getAllClaimsFromToken(token);
            Integer tokenVersion = claims.get("version", Integer.class);
            int currentVersion = getUserTokenVersion(userId);
          
            return tokenVersion != null && tokenVersion == currentVersion;
        } catch (Exception e) {
            return false;
        }
    }
}

生成带版本的 Token:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
    User user = authenticate(request);
  
    // 获取当前版本
    int version = tokenVersionService.getUserTokenVersion(user.getId().toString());
  
    Map<String, Object> claims = new HashMap<>();
    claims.put("role", user.getRole());
    claims.put("version", version);  // 添加版本号
  
    String token = jwtUtil.generateAccessToken(user.getId().toString(), claims);
  
    return ResponseEntity.ok(new AuthResponse(token, null, 86400L));
}

强制用户重新登录:

@PostMapping("/admin/force-relogin/{userId}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> forceRelogin(@PathVariable String userId) {
    // 增加版本号,使该用户的所有 Token 失效
    tokenVersionService.incrementTokenVersion(userId);
  
    return ResponseEntity.ok("用户已被强制下线");
}

十三、监控与日志

13.1 Token 使用统计

package com.example.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class TokenMetricsService {

    private final RedisTemplate<String, Long> redisTemplate;

    /**
     * 记录 Token 生成
     */
    public void recordTokenGeneration(String userId) {
        String dateKey = "metrics:token:generated:" + LocalDate.now();
        String userKey = "metrics:token:user:" + userId + ":" + LocalDate.now();
      
        redisTemplate.opsForValue().increment(dateKey);
        redisTemplate.opsForValue().increment(userKey);
        redisTemplate.expire(dateKey, 30, TimeUnit.DAYS);
        redisTemplate.expire(userKey, 30, TimeUnit.DAYS);
    }

    /**
     * 记录 Token 验证
     */
    public void recordTokenValidation(String userId, boolean success) {
        String dateKey = success ? 
                "metrics:token:validated:" + LocalDate.now() :
                "metrics:token:failed:" + LocalDate.now();
      
        redisTemplate.opsForValue().increment(dateKey);
        redisTemplate.expire(dateKey, 30, TimeUnit.DAYS);
    }

    /**
     * 记录 Token 刷新
     */
    public void recordTokenRefresh(String userId) {
        String dateKey = "metrics:token:refreshed:" + LocalDate.now();
        redisTemplate.opsForValue().increment(dateKey);
        redisTemplate.expire(dateKey, 30, TimeUnit.DAYS);
    }

    /**
     * 获取今日统计
     */
    public Map<String, Long> getTodayMetrics() {
        String today = LocalDate.now().toString();
        Map<String, Long> metrics = new HashMap<>();
      
        metrics.put("generated", getMetric("metrics:token:generated:" + today));
        metrics.put("validated", getMetric("metrics:token:validated:" + today));
        metrics.put("failed", getMetric("metrics:token:failed:" + today));
        metrics.put("refreshed", getMetric("metrics:token:refreshed:" + today));
      
        return metrics;
    }

    private Long getMetric(String key) {
        Long value = redisTemplate.opsForValue().get(key);
        return value != null ? value : 0L;
    }
}

13.2 审计日志

package com.example.entity;

import lombok.Data;

import javax.persistence.*;
import java.time.Instant;

@Entity
@Table(name = "audit_logs")
@Data
public class AuditLog {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String userId;

    @Column(nullable = false)
    private String action;  // LOGIN, LOGOUT, TOKEN_REFRESH, ACCESS_DENIED

    @Column(nullable = false)
    private String ipAddress;

    private String userAgent;

    private String resource;  // 访问的资源

    private Boolean success;

    @Column(columnDefinition = "TEXT")
    private String details;

    @Column(name = "created_at")
    private Instant createdAt = Instant.now();
}
package com.example.service;

import com.example.entity.AuditLog;
import com.example.repository.AuditLogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

@Service
@RequiredArgsConstructor
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Async
    public void logLogin(String userId, HttpServletRequest request, boolean success) {
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction("LOGIN");
        log.setIpAddress(getClientIp(request));
        log.setUserAgent(request.getHeader("User-Agent"));
        log.setSuccess(success);
      
        auditLogRepository.save(log);
    }

    @Async
    public void logTokenRefresh(String userId, HttpServletRequest request) {
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction("TOKEN_REFRESH");
        log.setIpAddress(getClientIp(request));
        log.setUserAgent(request.getHeader("User-Agent"));
        log.setSuccess(true);
      
        auditLogRepository.save(log);
    }

    @Async
    public void logAccessDenied(String userId, String resource, HttpServletRequest request) {
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction("ACCESS_DENIED");
        log.setResource(resource);
        log.setIpAddress(getClientIp(request));
        log.setUserAgent(request.getHeader("User-Agent"));
        log.setSuccess(false);
      
        auditLogRepository.save(log);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

13.3 监控端点

package com.example.controller;

import com.example.service.TokenMetricsService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/admin/metrics")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class MetricsController {

    private final TokenMetricsService tokenMetricsService;

    @GetMapping("/tokens/today")
    public Map<String, Long> getTodayTokenMetrics() {
        return tokenMetricsService.getTodayMetrics();
    }

    @GetMapping("/health")
    public Map<String, Object> healthCheck() {
        Map<String, Object> health = new HashMap<>();
        health.put("status", "UP");
        health.put("timestamp", System.currentTimeMillis());
      
        // 检查 Redis 连接
        try {
            redisTemplate.opsForValue().get("health:check");
            health.put("redis", "UP");
        } catch (Exception e) {
            health.put("redis", "DOWN");
        }
      
        return health;
    }
}

十四、完整项目结构

src/main/java/com/example/
├── config/
│   ├── SecurityConfig.java              # Spring Security 配置
│   ├── RedisConfig.java                 # Redis 配置
│   └── AsyncConfig.java                 # 异步配置
├── controller/
│   ├── AuthController.java              # 认证控制器
│   ├── UserController.java              # 用户控制器
│   └── MetricsController.java           # 监控控制器
├── entity/
│   ├── User.java                        # 用户实体
│   ├── RefreshToken.java                # Refresh Token 实体
│   └── AuditLog.java                    # 审计日志实体
├── repository/
│   ├── UserRepository.java
│   ├── RefreshTokenRepository.java
│   └── AuditLogRepository.java
├── service/
│   ├── UserService.java
│   ├── RefreshTokenService.java
│   ├── TokenBlacklistService.java       # Token 黑名单
│   ├── TokenVersionService.java         # Token 版本控制
│   ├── TokenMetricsService.java         # Token 统计
│   ├── LoginAttemptService.java         # 登录尝试限制
│   └── AuditService.java                # 审计日志
├── filter/
│   ├── JwtAuthenticationFilter.java     # JWT 认证过滤器
│   └── TokenRefreshFilter.java          # Token 自动刷新过滤器
├── util/
│   ├── JwtUtil.java                     # JWT 工具类
│   └── PermissionUtil.java              # 权限工具类
├── dto/
│   ├── LoginRequest.java
│   ├── AuthResponse.java
│   └── RefreshTokenRequest.java
├── exception/
│   ├── JwtException.java
│   ├── TokenExpiredException.java
│   ├── InvalidTokenException.java
│   └── GlobalExceptionHandler.java      # 全局异常处理
└── Application.java                     # 启动类

src/main/resources/
├── application.yml                      # 配置文件
└── application-prod.yml                 # 生产环境配置

十五、生产环境部署建议

15.1 环境变量配置

# application-prod.yml
jwt:
  secret: ${JWT_SECRET}  # 从环境变量读取
  access-token-expiration: ${JWT_ACCESS_EXPIRATION:900000}
  refresh-token-expiration: ${JWT_REFRESH_EXPIRATION:604800000}
  issuer: ${SPRING_APPLICATION_NAME}

spring:
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD}
    database: ${REDIS_DATABASE:0}
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

logging:
  level:
    com.example: INFO
  file:
    name: /var/log/app/application.log

15.2 Docker 部署

# Dockerfile
FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/jwt-demo-1.0.0.jar app.jar

ENV JWT_SECRET=""
ENV REDIS_HOST="redis"
ENV REDIS_PASSWORD=""

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - JWT_SECRET=${JWT_SECRET}
      - REDIS_HOST=redis
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    depends_on:
      - redis
      - mysql

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data

  mysql:
    image: mysql:8
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=jwt_demo
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  redis-data:
  mysql-data:

15.3 Kubernetes 部署

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jwt-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: jwt-app
  template:
    metadata:
      labels:
        app: jwt-app
    spec:
      containers:
      - name: jwt-app
        image: your-registry/jwt-app:latest
        ports:
        - containerPort: 8080
        env:
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: jwt-secrets
              key: jwt-secret
        - name: REDIS_HOST
          value: "redis-service"
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: redis-secrets
              key: password
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: jwt-app-service
spec:
  selector:
    app: jwt-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

十六、总结

16.1 JWT 核心要点

  1. 结构:Header.Payload.Signature
  2. 无状态:服务器不存储 Token
  3. 自包含:Token 包含所有必要信息
  4. 跨域友好:通过 HTTP Header 传递
  5. 易扩展:适合分布式系统

16.2 安全建议

  • ✅ 使用 HTTPS
  • ✅ 密钥安全存储
  • ✅ 设置合理过期时间
  • ✅ 实现 Token 刷新机制
  • ✅ 添加 Token 黑名单
  • ✅ 限制登录失败次数
  • ✅ 记录审计日志
  • ✅ 使用强加密算法

16.3 性能优化

  • ✅ 使用 Redis 缓存
  • ✅ 异步处理日志
  • ✅ Token 验证缓存
  • ✅ 连接池优化

16.4 适用场景

场景推荐方案
前后端分离JWT
微服务架构JWT
移动端 AppJWT
单体应用Session 或 JWT
高安全要求JWT + Refresh Token + 黑名单
需要踢人功能JWT + Token 版本控制

希望这份详尽的 Java JWT 技术详解能帮助您深入理解和实践 JWT 认证机制!如有任何疑问,欢迎继续交流。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇