Spring Boot + OAuth2 Token刷新和登录超时解决方案


在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过期时无感知地保持登录状态,同时保证系统的安全性。


文章作者: William
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 William !
评论
 本篇
Spring Boot + OAuth2 Token刷新和登录超时解决方案 Spring Boot + OAuth2 Token刷新和登录超时解决方案
在Spring Boot + Spring Security + Gateway + OAuth2的架构中,解决token刷新和登录超时问题需要综合考虑多个方面,本文将进行详细介绍。
2025-09-26
下一篇 
typora-vue-theme主题介绍 typora-vue-theme主题介绍
这是你自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要
2018-09-07
  目录