【小技巧】spring security oauth2 令牌實現多終端登錄狀態同步

  • 2019 年 10 月 6 日
  • 筆記
image

目的說明

解決不同客戶端使用token,各個客戶端的登錄狀態必須保持一致,退出狀態實現一致。同上述問題類似如何解決不同租戶相同用戶名的人員的登錄狀態問題。

默認的DefaultTokenServices 創建邏輯

	@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;    	}

判斷當前用戶是否存在token

我們來看 RedisTokenStore 的默認邏輯,注意Token key 的生成邏輯

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;  }

DefaultAuthenticationKeyGenerator拼接key

  • 主要參考一下 當前用戶的 username clientId scope ,這樣導致不同客戶端的token 不一致,某個客戶端退出不會影響其他客戶端
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 的生成規則

public class PigxAuthenticationKeyGenerator 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 即可實現如上效果

	@Bean  	public TokenStore tokenStore() {  		RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);  		tokenStore.setPrefix(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX);  		tokenStore.setAuthenticationKeyGenerator(new PigxAuthenticationKeyGenerator());  		return tokenStore;  	}

總結