™技术博客

框架 | 10.oauth2令牌实现多终端登录状态同步

2021年8月11日

目的说明


解决不同客户端使用token,各个客户端的登录状态必须保持一致,退出状态实现一致。同上述问题类似如何解决不同租户相同用户名的人员的登录状态问题。

1.默认的DefaultTokenServices 创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 1. 判断是否存在Token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}

// 2. 创建新token
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;

}

2.判断当前用户是否存在token

我们来看 RedisTokenStore 的默认逻辑,注意Token key 的生成逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);


@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
// 构造 默认保存的
String key = authenticationKeyGenerator.extractKey(authentication);
// key 加上前缀
byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
byte[] bytes = null;
RedisConnection conn = getConnection();
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
if (accessToken != null) {
OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
storeAccessToken(accessToken, authentication);
}

}
return accessToken;
}

3. DefaultAuthenticationKeyGenerator

  • 主要参考一下 当前用户的 username clientId scope ,这样导致不同客户端的token 不一致,某个客户端退出不会影响其他客户端
1
2
3
4
5
6
7
8
9
10
11
12
public String extractKey(OAuth2Authentication authentication) {
Map<String, String> values = new LinkedHashMap<String, String>();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put(USERNAME, authentication.getName());
}
values.put(CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
}
return generateKey(values);
}

重写token key 的生成规则


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AiopsAuthenticationKeyGenerator extends DefaultAuthenticationKeyGenerator {

private static final String SCOPE = "scope";

private static final String USERNAME = "username";

@Override
public String extractKey(OAuth2Authentication authentication) {
Map<String, String> values = new LinkedHashMap<String, String>();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put(USERNAME, authentication.getName());
}
if (authorizationRequest.getScope() != null) {
values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
}

// 如果是多租户系统,这里要区分租户ID 条件
return generateKey(values);
}
}

注入tokenstroe 即可实现如上效果

1
2
3
4
5
6
7
@Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(SecurityConstants.AIOPS_PREFIX + SecurityConstants.OAUTH_PREFIX);
tokenStore.setAuthenticationKeyGenerator(new AiopsAuthenticationKeyGenerator());
return tokenStore;
}

扫描二维码,分享此文章