有時候我們希望用戶只能在一臺設備登錄賬號(我們太吝嗇了)。
使用 springboot oauth2 怎麼實現呢?
注意本文不會帶你使用 spring security 實現 oauth2 登錄,僅僅是討論我們那個吝嗇的需求。
假設我們有這樣一個自定義的認證實現類:
1public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
2
3 private final static Long TIMEOUT = 10L;
4
5 private static final String AUTHORIZATION = "token";
6
7 private final RedisTemplate<String, Object> redisTemplate;
8
9 @Override
10 public void save(OAuth2Authorization authorization) {
11 // is refresh token mode or code mode
12 // ...
13 // is access token mode
14 if (isAccessToken(authorization)) {
15 OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
16 long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
17 redisTemplate.setValueSerializer(RedisSerializer.java());
18 redisTemplate.opsForValue()
19 .set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between,
20 TimeUnit.SECONDS);
21 }
22 }
23
24 @Override
25 public void remove(OAuth2Authorization authorization) {
26 // is refresh token mode or code mode
27 // ...
28 // is access token mode
29 if (isAccessToken(authorization)) {
30 OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
31 keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
32 }
33 redisTemplate.delete(keys);
34 }
35 @Override
36 @Nullable
37 public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
38 Assert.hasText(token, "token cannot be empty");
39 Assert.notNull(tokenType, "tokenType cannot be empty");
40 redisTemplate.setValueSerializer(RedisSerializer.java());
41 return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
42 }
43
44 private String buildKey(String type, String id) {
45 return String.format("%s::%s::%s", AUTHORIZATION, type, id);
46 }
47 // ...
48}
它將 token 存儲到 redis 中。key 是下面這種格式:
1token::access_token::xxxxxxxxxxxxxxxxxxxxxxxx
當用戶登錄的時候,findByToken 會被 spring security 調用,從而找出用戶信息。
因爲我們很吝嗇,我們希望一個用戶只能登錄一次,也就是說,一個用戶只有一個 token。
redis 的 key 不能再使用 token 了,而應該改成用戶名。
1token::access_token::that_annoying_user
但是這樣 findByToken 又如何根據 token 找出用戶名呢?採用這種 key,我們就沒有一個效率比較高的方法,能夠在數百萬用戶中找出該用戶。
那麼這樣的 key 怎麼樣?
1token::access_token::xxxxxxxxxxxxxxxxxxxxxxxx::that_annoying_user
既有 token,又有用戶信息。
假設用戶已經登錄,獲得了 xxxxxxxxxxxxxxxxxxxxxxxx
的 token。
他又來登錄我們的應用了,我們讓他登錄前,找出該用戶名下的舊的 token,並刪除。
1keys token::access_token::*::that_annoying_user
這樣配合應用中的檢查 token,就可以踢出他原來的登錄會話了。
1public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
2 @Override
3 public void save(OAuth2Authorization authorization) {
4 // is refresh token mode or code mode
5 // ...
6 // is access token mode
7 if (isAccessToken(authorization)) {
8 OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
9 long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
10 redisTemplate.setValueSerializer(RedisSerializer.java());
11
12 // 刪除該用戶的舊的 token
13 // token::access_token::*::userName
14 // 確保你的用戶名不允許 * 的存在
15 Set<String> keys = redisTemplate.keys(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, "*", authorization.getPrincipalName()));
16 if (! CollectionUtils.isEmpty(keys)) {
17 redisTemplate.delete(keys);
18 }
19
20 redisTemplate.opsForValue()
21 .set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue(), authorization.getPrincipalName()), authorization, between,
22 TimeUnit.SECONDS);
23 }
24 }
25
26 @Override
27 public void remove(OAuth2Authorization authorization) {
28 // is refresh token mode or code mode
29 // ...
30 // is access token mode
31 if (isAccessToken(authorization)) {
32 OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
33 keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue(), authorization.getPrincipalName()));
34 }
35 redisTemplate.delete(keys);
36 }
37
38 @Override
39 @Nullable
40 public OAuth2Authorization findById(String id) {
41 throw new UnsupportedOperationException();
42 }
43
44 @Override
45 @Nullable
46 public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
47 Assert.hasText(token, "token cannot be empty");
48 Assert.notNull(tokenType, "tokenType cannot be empty");
49 redisTemplate.setValueSerializer(RedisSerializer.java());
50
51 // token::access_token::tokenValue::*
52 Set<String> keys = redisTemplate.keys(buildKey(tokenType.getValue(), token, "*"));
53 if (CollectionUtils.isEmpty(keys)) {
54 return null;
55 }
56
57 List<Object> saved = redisTemplate.opsForValue().multiGet(keys);
58 if (CollectionUtils.isEmpty(saved)) {
59 return null;
60 }
61
62 return (OAuth2Authorization) saved.get(0);
63 }
64
65 private String buildKey(String type, String id, String principle) { // 增加了用戶名
66 return String.format("%s::%s::%s::%s", AUTHORIZATION, type, id, principle);
67 }
68 // ...
69}