在Spring Boot + Spring Security + Gateway + OAuth2的架构中,解决token刷新和登录超时问题需要综合考虑多个方面。以下是完整的解决方案:
1. Token过期时间配置
JWT Token配置
# application.yml
security:
oauth2:
client:
access-token-validity-seconds: 3600 # 1小时
refresh-token-validity-seconds: 2592000 # 30天
authorization:
token:
jwt:
sign-key: your-secret-key
Token Store配置
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("your-secret-key");
return converter;
}
}
2. 自动刷新Token机制
前端自动刷新逻辑
// 前端拦截器
axios.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 401) {
// Token过期,尝试刷新
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
try {
const response = await axios.post('/auth/refresh', {
refresh_token: refreshToken
});
const newAccessToken = response.data.access_token;
localStorage.setItem('access_token', newAccessToken);
// 重试原始请求
error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axios.request(error.config);
} catch (refreshError) {
// 刷新失败,跳转登录页
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
后端Refresh Token端点
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
try {
OAuth2AccessToken newToken = tokenServices.refreshAccessToken(
request.getRefreshToken(),
new DefaultOAuth2Request()
);
return ResponseEntity.ok(newToken);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
3. Gateway层面的Token处理
Token刷新过滤器
@Component
public class TokenRefreshFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
// 检查token是否即将过期(例如5分钟内)
if (isTokenExpiringSoon(token)) {
return refreshAndRetry(exchange, chain, token);
}
}
return chain.filter(exchange);
}
private boolean isTokenExpiringSoon(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey("your-secret-key")
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
long diff = expiration.getTime() - System.currentTimeMillis();
return diff < 5 * 60 * 1000; // 5分钟内过期
} catch (Exception e) {
return true;
}
}
private Mono<Void> refreshAndRetry(ServerWebExchange exchange,
GatewayFilterChain chain, String token) {
// 从Redis获取refresh token
String refreshToken = (String) redisTemplate.opsForValue()
.get("refresh_token:" + getUsernameFromToken(token));
if (refreshToken != null) {
return refreshToken(refreshToken)
.flatMap(newToken -> {
// 使用新token重试请求
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header("Authorization", "Bearer " + newToken)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
});
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 高优先级
}
}
4. Redis存储Token信息
Redis配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
Token存储服务
@Service
public class TokenStoreService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
private static final String ACCESS_TOKEN_PREFIX = "access_token:";
public void storeTokens(String username, String accessToken, String refreshToken,
Duration accessTokenTimeout, Duration refreshTokenTimeout) {
redisTemplate.opsForValue().set(
ACCESS_TOKEN_PREFIX + username,
accessToken,
accessTokenTimeout
);
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + username,
refreshToken,
refreshTokenTimeout
);
}
public String getRefreshToken(String username) {
return (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + username);
}
}
5. 安全配置优化
Resource Server配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/auth/refresh").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
6. 心跳机制保持会话
前端心跳检测
class TokenManager {
constructor() {
this.heartbeatInterval = 10 * 60 * 1000; // 10分钟
this.startHeartbeat();
}
startHeartbeat() {
setInterval(() => {
this.checkTokenValidity();
}, this.heartbeatInterval);
}
async checkTokenValidity() {
const token = localStorage.getItem('access_token');
if (!token) return;
try {
// 发送一个简单的请求检查token有效性
await axios.get('/api/heartbeat', {
headers: { 'Authorization': `Bearer ${token}` }
});
} catch (error) {
if (error.response.status === 401) {
this.refreshToken();
}
}
}
async refreshToken() {
// 刷新token逻辑
}
}
7. 监控和日志
Token过期监控
@Component
public class TokenExpirationMonitor {
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorTokenExpiration() {
// 监控即将过期的token,发送通知或自动刷新
}
}
关键注意事项
- 安全性:Refresh token应该有更严格的安全保护
- 并发控制:处理多个请求同时刷新token的情况
- 降级策略:刷新失败时的用户体验处理
- 性能考虑:避免频繁的token刷新影响性能
这样的方案可以确保用户在token过期时无感知地保持登录状态,同时保证系统的安全性。