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}