网站首页 > 技术文章 正文
Spring Boot 3.x 中的 Token 续期方案实现
在 Spring Boot 3.x 中结合 Spring Security 6.x 和 OAuth2,可以实现多种 Token 续期策略,常见有:
- 刷新令牌(Refresh Token)
- 滑动过期(Sliding Expiration)
- 静默续期(Silent Renewal)
下面将完整实现这三种方案,并加上生产级安全增强。
1. 项目依赖
<dependencies>
<!-- Spring Security + OAuth2 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- Redis 存储 Token 黑名单和元数据 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
2. JWT 配置(公钥 / 私钥)
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
@Bean
public JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
3. 安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private SlidingExpirationFilter slidingExpirationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/token", "/auth/refresh").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.addFilterBefore(slidingExpirationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. Token 服务(生成 & 解析 & 黑名单)
@Service
public class TokenService {
@Value("${jwt.expiration}")
private long jwtExpiration;
@Value("${jwt.refresh-expiration}")
private long refreshExpiration;
@Autowired
private JwtEncoder jwtEncoder;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String TOKEN_BLACKLIST = "token:blacklist:";
public String generateAccessToken(UserDetails userDetails) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("your-issuer")
.issuedAt(now)
.expiresAt(now.plusMillis(jwtExpiration))
.subject(userDetails.getUsername())
.claim("scope", "ROLE_USER")
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
public String generateRefreshToken(UserDetails userDetails) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("your-issuer")
.issuedAt(now)
.expiresAt(now.plusMillis(refreshExpiration))
.subject(userDetails.getUsername())
.claim("token_type", "refresh")
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
public void blacklistToken(String token, long expiresInSeconds) {
redisTemplate.opsForValue().set(TOKEN_BLACKLIST + token, "1", expiresInSeconds, TimeUnit.SECONDS);
}
public boolean isTokenBlacklisted(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(TOKEN_BLACKLIST + token));
}
}
5. 刷新令牌端点
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private TokenService tokenService;
@Autowired
private JwtDecoder jwtDecoder;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/refresh")
public ResponseEntity<JwtResponse> refreshToken(@RequestBody RefreshRequest request) {
try {
Jwt jwt = jwtDecoder.decode(request.getRefreshToken());
if (!"refresh".equals(jwt.getClaims().get("token_type"))) {
throw new JwtException("Invalid token type");
}
if (tokenService.isTokenBlacklisted(request.getRefreshToken())) {
throw new JwtException("Token already used");
}
UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject());
String newAccessToken = tokenService.generateAccessToken(userDetails);
String newRefreshToken = tokenService.generateRefreshToken(userDetails);
// 旧 refresh token 加入黑名单
long ttl = jwt.getExpiresAt().getEpochSecond() - Instant.now().getEpochSecond();
tokenService.blacklistToken(request.getRefreshToken(), ttl);
return ResponseEntity.ok(new JwtResponse(newAccessToken, newRefreshToken, "Bearer",
jwt.getExpiresAt().getEpochSecond()));
} catch (JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
6. 滑动过期 Filter
@Component
public class SlidingExpirationFilter extends OncePerRequestFilter {
@Value("${jwt.sliding-threshold-seconds:300}")
private long slidingThresholdSeconds;
@Autowired
private JwtDecoder jwtDecoder;
@Autowired
private TokenService tokenService;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
if (tokenService.isTokenBlacklisted(token)) {
throw new JwtException("Blacklisted token");
}
Jwt jwt = jwtDecoder.decode(token);
long expiresIn = jwt.getExpiresAt().getEpochSecond() - Instant.now().getEpochSecond();
if (expiresIn < slidingThresholdSeconds) {
UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject());
String newToken = tokenService.generateAccessToken(userDetails);
response.setHeader("X-Renewed-Token", newToken);
}
} catch (JwtException ignored) {}
}
filterChain.doFilter(request, response);
}
}
7. 静默续期(前端)
function checkTokenExpiration() {
const token = getTokenFromStorage();
if (token && isTokenExpiringSoon(token)) {
renewTokenSilently();
}
}
function renewTokenSilently() {
const refreshToken = getRefreshTokenFromStorage();
fetch('/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(data => storeNewTokens(data.accessToken, data.refreshToken))
.catch(() => redirectToLogin());
}
setInterval(checkTokenExpiration, 60000);
8. 配置文件示例
# Token 过期时间(毫秒)
jwt.expiration=900000 # 15分钟
jwt.refresh-expiration=2592000000 # 30天
jwt.sliding-threshold-seconds=300 # 滑动续期阈值(秒)
spring.redis.host=127.0.0.1
spring.redis.port=6379
9. 最佳实践建议
- Access Token 短期有效,Refresh Token 长期有效(短期:5
15分钟,长期:730天) - Refresh Token 单次使用,刷新后立即作废旧的
- 引入黑名单机制,支持即时下线和注销
- 滑动过期阈值可配置,减少不必要的刷新
- 跨端校验 Refresh Token,绑定 client_id、device_id、ip
- 多节点部署共享 Redis,保持一致性
这样整合后,你的项目既有 标准 OAuth2 刷新令牌方案,又有 滑动过期 和 静默续期 作为补充,并且具备 黑名单安全机制,可以直接落地到生产。
猜你喜欢
- 2025-10-02 加拿大留学毕业了学习许可将到期工作许可还没发,算合法居住吗?
- 2025-10-02 CTO:通电全司,前端入关!_cto干嘛的
- 2025-10-02 前端全栈转型必修NestJS从TypeScript基础到模块化服务端架构实战
- 2025-10-02 Int'l community hopes US, Russia to fulfill nuclear disarmament responsibility: Chinese foreign ministry
- 2024-08-08 SSM框架警校在线考试系统的设计与开发
- 2024-08-08 持续集成CICD实战5—提交测试成功镜像到harbor镜像仓库
- 2024-08-08 如何在分布式环境中搭建单点登录系统|第二篇:SSO核心代码
- 2024-08-08 一文带你了解 OAuth2 协议与 Spring Security OAuth2 集成
- 2024-08-08 使用Oauth2实现微服务的安全保护(微服务 接口安全)
- 2024-08-08 OAuth 系列(四)密码模式 Resource Owner Password Credentials
- 10-02基于深度学习的铸件缺陷检测_如何控制和检测铸件缺陷?有缺陷铸件如何处置?
- 10-02Linux Mint 22.1 Cinnamon Edition 搭建深度学习环境
- 10-02AWD-LSTM语言模型是如何实现的_lstm语言模型
- 10-02NVIDIA Jetson Nano 2GB 系列文章(53):TAO模型训练工具简介
- 10-02使用ONNX和Torchscript加快推理速度的测试
- 10-02tensorflow GPU环境安装踩坑日记_tensorflow配置gpu环境
- 10-02Keye-VL-1.5-8B 快手 Keye-VL— 腾讯云两卡 32GB GPU保姆级部署指南
- 10-02Gateway_gateways
- 最近发表
-
- 基于深度学习的铸件缺陷检测_如何控制和检测铸件缺陷?有缺陷铸件如何处置?
- Linux Mint 22.1 Cinnamon Edition 搭建深度学习环境
- AWD-LSTM语言模型是如何实现的_lstm语言模型
- NVIDIA Jetson Nano 2GB 系列文章(53):TAO模型训练工具简介
- 使用ONNX和Torchscript加快推理速度的测试
- tensorflow GPU环境安装踩坑日记_tensorflow配置gpu环境
- Keye-VL-1.5-8B 快手 Keye-VL— 腾讯云两卡 32GB GPU保姆级部署指南
- Gateway_gateways
- Coze开源本地部署教程_开源canopen
- 扣子开源本地部署教程 丨Coze智能体小白喂饭级指南
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)