Oauth2 限制登录一个客户端

Oauth2 限制登录一个客户端

有时候我们希望用户只能在一台设备登录账号(我们太吝啬了)。

使用 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}