spring - 微服务认证 5 刷新token
·
Spring Security 实现自定义刷新令牌机制
1. 自定义令牌实体类
// 令牌响应DTO
@Data
@AllArgsConstructor
public class TokenResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
private Long expiresIn; // 秒
private Long refreshExpiresIn;
}
// 刷新请求DTO
@Data
public class RefreshRequest {
@NotBlank
private String refreshToken;
}
// 数据库刷新令牌实体
@Entity
@Table(name = "refresh_tokens")
@Data
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 512)
private String tokenHash; // 存储哈希值
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String clientInfo; // IP + User-Agent 的哈希
@Column(nullable = false)
private Instant expiresAt;
@Column(nullable = false)
private Instant createdAt;
@Column(nullable = false)
private boolean revoked = false;
@Column
private Instant revokedAt;
}
2. 自定义令牌服务
@Service
@Transactional
@Slf4j
public class JwtTokenService {
@Value("${app.jwt.secret:defaultSecretKey}")
private String jwtSecret;
@Value("${app.jwt.access-token-expiration:900}") // 15分钟
private Long accessTokenExpiration;
@Value("${app.jwt.refresh-token-expiration:2592000}") // 30天
private Long refreshTokenExpiration;
private final RefreshTokenRepository refreshTokenRepository;
private final UserRepository userRepository;
public JwtTokenService(RefreshTokenRepository refreshTokenRepository,
UserRepository userRepository) {
this.refreshTokenRepository = refreshTokenRepository;
this.userRepository = userRepository;
}
// 生成访问令牌 (JWT)
public String generateAccessToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration * 1000))
.signWith(SignatureAlgorithm.HS256, jwtSecret)
.compact();
}
// 生成刷新令牌 (不透明)
public String generateRefreshToken() {
return UUID.randomUUID().toString().replace("-", "") +
UUID.randomUUID().toString().replace("-", "");
}
// 创建令牌对
public TokenResponse generateTokenPair(User user, HttpServletRequest request) {
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken();
// 保存刷新令牌到数据库
saveRefreshToken(refreshToken, user, request);
return new TokenResponse(
accessToken,
refreshToken,
accessTokenExpiration,
refreshTokenExpiration
);
}
private void saveRefreshToken(String refreshToken, User user, HttpServletRequest request) {
String tokenHash = hashToken(refreshToken);
String clientFingerprint = createClientFingerprint(request);
RefreshToken token = new RefreshToken();
token.setTokenHash(tokenHash);
token.setUser(user);
token.setClientInfo(clientFingerprint);
token.setCreatedAt(Instant.now());
token.setExpiresAt(Instant.now().plusSeconds(refreshTokenExpiration));
token.setRevoked(false);
refreshTokenRepository.save(token);
// 清理过期的令牌
cleanupExpiredTokens(user.getId());
}
// 刷新令牌
public TokenResponse refreshToken(String refreshTokenValue, HttpServletRequest request) {
// 1. 查找令牌
String tokenHash = hashToken(refreshTokenValue);
RefreshToken refreshToken = refreshTokenRepository.findByTokenHash(tokenHash)
.orElseThrow(() -> new SecurityException("无效的刷新令牌"));
// 2. 验证令牌状态
validateRefreshToken(refreshToken, request);
// 3. 令牌轮换 - 撤销旧令牌
refreshToken.setRevoked(true);
refreshToken.setRevokedAt(Instant.now());
refreshTokenRepository.save(refreshToken);
// 4. 生成新令牌对
return generateTokenPair(refreshToken.getUser(), request);
}
private void validateRefreshToken(RefreshToken refreshToken, HttpServletRequest request) {
// 检查是否已撤销
if (refreshToken.isRevoked()) {
log.warn("尝试使用已撤销的刷新令牌,用户: {}", refreshToken.getUser().getUsername());
throw new SecurityException("令牌已被撤销");
}
// 检查是否过期
if (Instant.now().isAfter(refreshToken.getExpiresAt())) {
// 自动清理过期令牌
refreshTokenRepository.delete(refreshToken);
throw new SecurityException("刷新令牌已过期");
}
// 客户端指纹验证 (可选,根据安全要求)
String currentFingerprint = createClientFingerprint(request);
if (!currentFingerprint.equals(refreshToken.getClientInfo())) {
log.warn("客户端指纹不匹配,可能为令牌盗用,用户: {}", refreshToken.getUser().getUsername());
// 根据安全策略决定:可以记录日志、撤销所有令牌或要求重新登录
handleSuspiciousActivity(refreshToken.getUser());
throw new SecurityException("安全验证失败");
}
}
private void handleSuspiciousActivity(User user) {
// 撤销用户的所有刷新令牌
refreshTokenRepository.revokeAllUserTokens(user.getId());
log.warn("检测到可疑活动,已撤销用户 {} 的所有令牌", user.getUsername());
// 可以在这里发送安全通知邮件
}
private String hashToken(String token) {
return DigestUtils.sha256Hex(token);
}
private String createClientFingerprint(HttpServletRequest request) {
String ip = getClientIp(request);
String userAgent = request.getHeader("User-Agent") != null ?
request.getHeader("User-Agent") : "";
// 使用IP段而不是完整IP,避免同一网络下IP变化
String ipSegment = ip.contains(".") ?
ip.substring(0, ip.lastIndexOf('.')) : ip;
return DigestUtils.sha256Hex(ipSegment + userAgent);
}
private String getClientIp(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader != null) {
return xfHeader.split(",")[0];
}
return request.getRemoteAddr();
}
private void cleanupExpiredTokens(Long userId) {
refreshTokenRepository.deleteExpiredTokens(Instant.now());
}
// 验证JWT访问令牌
public boolean validateAccessToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 从JWT中提取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 主动撤销令牌
public void revokeRefreshToken(String refreshToken) {
String tokenHash = hashToken(refreshToken);
refreshTokenRepository.findByTokenHash(tokenHash)
.ifPresent(token -> {
token.setRevoked(true);
token.setRevokedAt(Instant.now());
refreshTokenRepository.save(token);
});
}
// 撤销用户的所有令牌
public void revokeAllUserTokens(Long userId) {
refreshTokenRepository.revokeAllUserTokens(userId);
}
}
3. 自定义认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenService tokenService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenService tokenService,
UserDetailsService userDetailsService) {
this.tokenService = tokenService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenService.validateAccessToken(jwt)) {
String username = tokenService.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("无法设置用户认证", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
4. 控制器端点
@RestController
@RequestMapping("/api/auth")
@Validated
public class AuthController {
private final JwtTokenService tokenService;
private final AuthenticationManager authenticationManager;
public AuthController(JwtTokenService tokenService,
AuthenticationManager authenticationManager) {
this.tokenService = tokenService;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@Valid @RequestBody LoginRequest loginRequest,
HttpServletRequest request) {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌对
User user = (User) authentication.getPrincipal();
TokenResponse tokenResponse = tokenService.generateTokenPair(user, request);
return ResponseEntity.ok(tokenResponse);
}
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(@Valid @RequestBody RefreshRequest refreshRequest,
HttpServletRequest request) {
try {
TokenResponse tokenResponse = tokenService.refreshToken(refreshRequest.getRefreshToken(), request);
return ResponseEntity.ok(tokenResponse);
} catch (SecurityException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(@Valid @RequestBody RefreshRequest refreshRequest) {
tokenService.revokeRefreshToken(refreshRequest.getRefreshToken());
return ResponseEntity.ok().build();
}
@PostMapping("/logout-all")
public ResponseEntity<Void> logoutAll(Authentication authentication) {
User user = (User) authentication.getPrincipal();
tokenService.revokeAllUserTokens(user.getId());
return ResponseEntity.ok().build();
}
}
5. Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtTokenService tokenService;
private final UserDetailsService userDetailsService;
public SecurityConfig(JwtTokenService tokenService, UserDetailsService userDetailsService) {
this.tokenService = tokenService;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login", "/api/auth/refresh", "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(tokenService, userDetailsService);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6. Repository 接口
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByTokenHash(String tokenHash);
List<RefreshToken> findByUserIdAndRevokedFalse(Long userId);
@Modifying
@Query("UPDATE RefreshToken rt SET rt.revoked = true, rt.revokedAt = :revokedAt WHERE rt.user.id = :userId")
void revokeAllUserTokens(@Param("userId") Long userId);
@Modifying
@Query("DELETE FROM RefreshToken rt WHERE rt.expiresAt < :now")
void deleteExpiredTokens(@Param("now") Instant now);
@Query("SELECT COUNT(rt) FROM RefreshToken rt WHERE rt.user.id = :userId AND rt.revoked = false AND rt.createdAt > :since")
Long countActiveTokensSince(@Param("userId") Long userId, @Param("since") Instant since);
}
更多推荐

所有评论(0)