
优点:
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器端存储压力
缺点:需要自己实现
一、JWT 基础概念
1.1 什么是 JWT?
JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息的紧凑且自包含的方式。
1.2 JWT 的结构
JWT 由三部分组成,用 . 分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature

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-jwt | Auth0 官方库 | ⭐⭐⭐⭐ | 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 对比总结
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 扩展性 | ⭐⭐⭐⭐⭐ 无状态,易扩展 | ⭐⭐⭐ 需要共享存储 |
| 性能 | ⭐⭐⭐⭐ 无需查询 | ⭐⭐⭐ 需要查询存储 |
| 安全性 | ⭐⭐⭐ 无法主动失效 | ⭐⭐⭐⭐ 可主动销毁 |
| 跨域支持 | ⭐⭐⭐⭐⭐ 天然支持 | ⭐⭐⭐ 需要配置 |
| 移动端支持 | ⭐⭐⭐⭐⭐ 完美支持 | ⭐⭐⭐ 需要特殊处理 |
| 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 核心要点
- 结构:Header.Payload.Signature
- 无状态:服务器不存储 Token
- 自包含:Token 包含所有必要信息
- 跨域友好:通过 HTTP Header 传递
- 易扩展:适合分布式系统
16.2 安全建议
- ✅ 使用 HTTPS
- ✅ 密钥安全存储
- ✅ 设置合理过期时间
- ✅ 实现 Token 刷新机制
- ✅ 添加 Token 黑名单
- ✅ 限制登录失败次数
- ✅ 记录审计日志
- ✅ 使用强加密算法
16.3 性能优化
- ✅ 使用 Redis 缓存
- ✅ 异步处理日志
- ✅ Token 验证缓存
- ✅ 连接池优化
16.4 适用场景
| 场景 | 推荐方案 |
|---|---|
| 前后端分离 | JWT |
| 微服务架构 | JWT |
| 移动端 App | JWT |
| 单体应用 | Session 或 JWT |
| 高安全要求 | JWT + Refresh Token + 黑名单 |
| 需要踢人功能 | JWT + Token 版本控制 |
希望这份详尽的 Java JWT 技术详解能帮助您深入理解和实践 JWT 认证机制!如有任何疑问,欢迎继续交流。





