Spring Security OAuth2.0实战:多终端统一认证与JWT续签设计
【摘要】 Spring Security OAuth2.0实战:多终端统一认证与JWT续签设计 一、场景痛点:为什么需要“多终端统一认证+JWT续签”痛点说明多端重复登录PC、App、小程序各自为政,用户每换一个端就要重新输密码JWT无法续签传统JWT过期必须重新登录,体验差;若把有效期设得很长则不安全并发刷新竞争多终端同时刷新 token 时,容易出现“旧 token 失效、新 token 被覆盖...
Spring Security OAuth2.0实战:多终端统一认证与JWT续签设计
一、场景痛点:为什么需要“多终端统一认证+JWT续签”
痛点 | 说明 |
---|---|
多端重复登录 | PC、App、小程序各自为政,用户每换一个端就要重新输密码 |
JWT无法续签 | 传统JWT过期必须重新登录,体验差;若把有效期设得很长则不安全 |
并发刷新竞争 | 多终端同时刷新 token 时,容易出现“旧 token 失效、新 token 被覆盖” |
登出/踢人困难 | 无状态JWT让服务端无法主动失效令牌 |
二、总体架构设计
┌──────────────┐ ┌──────────────┐
│ 前端(三端) │ │ 网关层(GW) │
└─────┬────────┘ └──────┬───────┘
│ Authorization header │
│ (access_token) │
└──────────┬─────────────┘
│ 1. 校验JWT
│ 2. 若过期→去 Auth-Service 刷新
┌────────┴────────┐
│ Auth-Service │ ←─ Redis(计时Map防并发)
└────────┬────────┘
│ 3. 颁发
│ new_access_token
│ new_refresh_token
核心设计点
- 双 Token:短有效期
access_token
(15 min)+ 长有效期refresh_token
(7 d) - 计时 Map:30 s 内同一
refresh_token
只返回同一组 JWT,防止并发重复签发 - 统一用户状态:所有端共用
user_id
+device_id
作为 Redis key,可实现单点踢出
三、授权服务器(Authorization Server)实现
3.1 依赖 & 主启动类
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 JWT 公私钥配置
@Configuration
public class JwtKeyConfig {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
new ClassPathResource("jwt.jks"),
"123456".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(factory.getKeyPair("jwt"));
return converter;
}
}
3.3 授权服务器核心配置
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret(passwordEncoder().encode("web_secret"))
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.accessTokenValiditySeconds(900) // 15 min
.refreshTokenValiditySeconds(604800); // 7 d
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false); // 每次刷新都产生新refresh_token
}
}
3.4 计时 Map 防并发刷新
@Component
public class TokenRefreshGate {
private final RedisTemplate<String, String> redisTemplate;
private static final String REFRESH_LOCK = "refresh_lock:";
private static final Duration LOCK_TTL = Duration.ofSeconds(30);
public TokenRefreshGate(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean enter(String refreshTokenValue) {
String key = REFRESH_LOCK + refreshTokenValue;
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(key, "1", LOCK_TTL);
return Boolean.TRUE.equals(absent);
}
public void exit(String refreshTokenValue) {
redisTemplate.delete(REFRESH_LOCK + refreshTokenValue);
}
}
在自定义 TokenEndpoint
中调用 TokenRefreshGate
即可实现 30 s 内幂等返回同一 JWT。
四、资源服务器(Resource Server)实现
4.1 资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(new JwtTokenStore(jwtAccessTokenConverter));
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
}
}
4.2 网关层统一刷新逻辑
@Component
public class JwtRelayFilter extends GlobalFilter {
@Autowired
private TokenRefreshGate refreshGate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = extractToken(exchange);
if (token != null && JwtUtil.isExpired(token)) {
String refreshToken = extractRefreshTokenFromCookie(exchange);
if (refreshToken != null && refreshGate.enter(refreshToken)) {
try {
TokenPair newPair = refreshToken(refreshToken);
addTokenToHeader(exchange, newPair.getAccessToken());
addRefreshCookie(exchange, newPair.getRefreshToken());
} finally {
refreshGate.exit(refreshToken);
}
}
}
return chain.filter(exchange);
}
}
网关负责无感刷新,业务微服务无感知。
五、多端统一用户状态与踢人
Redis 结构
user:{userId}:tokens:{deviceId} -> {refreshTokenHash}
- 登录:每次颁发 refresh_token 前先写入 Redis,TTL=7 d
- 踢人:删除该 key,再发 Pub/Sub 消息通知网关层主动失效本地缓存
- 统计在线设备:通过
KEYS user:{userId}:tokens:*
即可列出所有端
示例代码
@Service
public class DeviceService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void kick(Long userId, String deviceId) {
String key = "user:" + userId + ":tokens:" + deviceId;
redisTemplate.delete(key);
redisTemplate.convertAndSend("kick_channel", userId + ":" + deviceId);
}
}
六、完整的刷新时序图
Parse error on line 18: ...token; Authorization: new_access -----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got 'TXT'七、测试用例(curl + jq)
- 密码模式获取令牌
curl -s -u web:web_secret -X POST \
http://localhost:8080/oauth/token \
-d "grant_type=password&username=admin&password=123456&scope=all" \
| jq '.access_token,.refresh_token'
- 刷新令牌
curl -s -u web:web_secret -X POST \
http://localhost:8080/oauth/token \
-d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN" \
| jq '.access_token'
- 踢出设备
curl -X DELETE http://localhost:8080/devices/kick?deviceId=iPhone12
八、小结与展望
- 通过“双 Token + 计时 Map”既解决了 JWT 续签,又避免了并发竞争
- 网关统一刷新,业务零侵入
- Redis 存储用户-设备映射,实现多端登录管理与强制下线
- 下一步可引入 Refresh Token Rotation(每次刷新都更换 refresh_token)进一步提升安全性,并结合 Device Fingerprint 防止 token 被盗用。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)