有时候我们希望用户只能在一台设备登录账号(我们太吝啬了)。
使用 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}