動態

詳情 返回 返回

spring security oauth2.0 sso流程分析 - 動態 詳情

現在我們系統使用的是spring security oauth2.0 sso單點登錄方案,偶爾出現會話失效,經過分析比對看了下日誌,出現會話失效時會出現一個警告,

Could not fetch user details: class org.springframework.security.oauth2.client.resource.UserRedirectRequiredException, A redirect is required to get the users approval

於是開始了spring security oauth2.0 sso的對接流程分析

首先記錄一下spring security的新架構
可以配置多個SecurityFilterChain 取第一個匹配的SecurityFilterChain,每個chain可以配置不同的filter

for(SecurityFilterChain chain : this.filterChains) {
    if (chain.matches(request)) {
        return chain.getFilters();
    }
}

以及spring security filter鏈的生命週期
有個FilterChainProxy的filter實現,然後裏面是spring security相關的filter鏈

開始oauth的分析
在oauth2.0的客户端配置@EnableOauth2Sso //開啓單點登錄
會註冊一個ResourceServerTokenServices的實現UserInfoTokenServices
和一個OAuth2ClientAuthenticationProcessingFilter的攔截器
攔截器會每次執行tokenServices.loadAuthentication(accessToken.getValue());

然後裏面會調用

private Map<String, Object> getMap(String path, String accessToken) {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Getting user info from: " + path);
    }
    try {
        OAuth2RestOperations restTemplate = this.restTemplate;
        if (restTemplate == null) {
            BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
            resource.setClientId(this.clientId);
            restTemplate = new OAuth2RestTemplate(resource);
        }
        OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
                .getAccessToken();
        if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
            DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
                    accessToken);
            token.setTokenType(this.tokenType);
            restTemplate.getOAuth2ClientContext().setAccessToken(token);
        }
        return restTemplate.getForEntity(path, Map.class).getBody();
    }
    catch (Exception ex) {
        this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
                + ex.getMessage());
        return Collections.<String, Object>singletonMap("error",
                "Could not fetch user details");
    }
}

這裏就可以看到警告日誌的產生處了
path就是oauth定義的

security:
  oauth2:
    resource:
      user-info-uri: http://ip:port/userinfo

看起來就是oauth的服務端接口返回錯誤了,進入到服務端的邏輯排查

資源端註解是@EnableResourceServer
會定義一個OAuth2AuthenticationProcessingFilter攔截器
攔截器判斷請求頭是否有Authorization 有則authenticationManager.authenticate進行認證
這裏面也有個tokenServices.loadAuthentication(token);
但這時tokenServices的實現是DefaultTokenServices
默認就是通過解析token(jwt)來獲取用户信息並得到會話,不過我們重新自定義了一下JwtAccessTokenConverter或者TokenStore等等,
實現加上redis過期、主動退出等邏輯判斷來統一管理token
這時預估就是redis的問題了,因為我們這個是偶然復現,如果是jwt解析有問題,那應該是必現的
於是經過排除,測試刪除redis的key(我們就是存的用户id)等場景終於復現了該問題
當刪除redis的key時,我們會拋出一個異常,從而導致userinfo的端點接口返回一個401
在客户端的resttemplate接收到該異常就會做出一個重新獲取accessToken的操作
OAuth2RestTemplate.createRequest -> OAuth2RestTemplate.getAccessToken -> OAuth2RestTemplate.acquireAccessToken -> accessTokenProvider.obtainAccessToken

這裏的accessTokenProvider就是AuthorizationCodeAccessTokenProvider(授權碼模式獲取accessToken),這個實現默認就是拋出一個重定向的異常
於是我們看到的日誌異常就是UserRedirectRequiredException

説明:
ResourceServerTokenServices有
UserInfoTokenServices實現和DefaultTokenServices實現
oauth2.0SSO的實現是UserInfoTokenServices,通過(客户端)調用userinfo接口獲取用户數據,從而實現單點登錄
DefaultTokenServices是通過(資源端)解析jwt來獲取用户數據,可以通過自定義JwtAccessTokenConverter或者TokenStore等等,加上redis過期、主動退出等邏輯判斷來實現統一管理token
accessTokenConverter.setJwtClaimsSetVerifier
JwtTokenStore.storeAccessToken和JwtTokenStore.removeAccessToken

user avatar jianxiangjie3rkv9 頭像 xiaoniuhululu 頭像 journey_64224c9377fd5 頭像 ahahan 頭像 zzzzbw 頭像 5n7qfpo1 頭像 boxuegu 頭像 qngyun1029 頭像 lvweifu 頭像 shenchendexiaoyanyao 頭像 jkdataapi 頭像 javaedge 頭像
點贊 19 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.