spring security與oauth2集成實現對多服務系統的認證與授權


系統架構流程

 

 

服務模塊可以有多個,認證和授權是做在一起的單獨一個模塊。

 

 

原本想寫關於spring security的源碼閱讀的文章,但是一方面考慮時間問題,另一方面出於實用的目的,這里打算記錄一下相關啟動加載流程,請求認證和授權的流程。

概要如下:

1、啟動流程概述

2、token獲取流程分析

3、請求的認證與授權流程分析

 

一、啟動流程概述

  此處參考這里

  啟動流程的關注點主要涉及配置信息的加載過濾器鏈的構建

spring security框架核心就是這個過濾器鏈!它是分析流程始和調試代碼終要關心的重點。

首先看啟動入口類org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration,方法setFilterChainProxySecurityConfigurer中有

for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
            webSecurity.apply(webSecurityConfigurer);
        }

會將org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter類型的配置(security相關)、org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter類型配置(資源服務器相關)、org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter類型配置(授權服務器相關)都裝配到org.springframework.security.config.annotation.web.builders.WebSecurity對象中。

繼續關注方法springSecurityFilterChain

 1 public Filter springSecurityFilterChain() throws Exception {
 2         boolean hasConfigurers = webSecurityConfigurers != null
 3                 && !webSecurityConfigurers.isEmpty();
 4         if (!hasConfigurers) {
 5             WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
 6                     .postProcess(new WebSecurityConfigurerAdapter() {
 7                     });
 8             webSecurity.apply(adapter);
 9         }
10         return webSecurity.build();
11     }

跟進第10行,進入org.springframework.security.config.annotation.AbstractSecurityBuilder#build,

1 public final O build() throws Exception {
2         if (this.building.compareAndSet(false, true)) {
3             this.object = doBuild();
4             return this.object;
5         }
6         throw new AlreadyBuiltException("This object has already been built");
7     }

繼續進入doBuild()方法,

 1 protected final O doBuild() throws Exception {
 2         synchronized (configurers) {
 3             buildState = BuildState.INITIALIZING;
 4 
 5             beforeInit();
 6             init();
 7 
 8             buildState = BuildState.CONFIGURING;
 9 
10             beforeConfigure();
11             configure();
12 
13             buildState = BuildState.BUILDING;
14 
15             O result = performBuild();
16 
17             buildState = BuildState.BUILT;
18 
19             return result;
20         }
21     }

由於spring security的配置一般都是繼承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter,這里針對它來看。重點看上面的第6行init(),第11行configure(),第15行performBuild()。init()方法會觸發org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#init方法,

 1 public void init(final WebSecurity web) throws Exception {
 2         final HttpSecurity http = getHttp();
 3         web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
 4             public void run() {
 5                 FilterSecurityInterceptor securityInterceptor = http
 6                         .getSharedObject(FilterSecurityInterceptor.class);
 7                 web.securityInterceptor(securityInterceptor);
 8             }
 9         });
10     }

接着看getHttp()方法,其中含有的如下代碼會構建默認過濾器鏈,

 1 http
 2                 .csrf().and()
 3                 .addFilter(new WebAsyncManagerIntegrationFilter())
 4                 .exceptionHandling().and()
 5                 .headers().and()
 6                 .sessionManagement().and()
 7                 .securityContext().and()
 8                 .requestCache().and()
 9                 .anonymous().and()
10                 .servletApi().and()
11                 .apply(new DefaultLoginPageConfigurer<>()).and()
12                 .logout();

可通過debug查看每一步對應構建出來的過濾器,如下圖:

同時getHttp()方法最后有

1 configure(http)

這里會執行org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter類型的配置(security相關)、org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter類型配置(資源服務器相關)、org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter類型配置(授權服務器相關)許多configure方法。

到此大致解釋了配置信息的加載流程和默認過濾器鏈的創建。

 

二、token獲取流程分析

  本文是使用oauth2的密碼授權模式(Password Grant Type),簡言之,首次認證時用戶傳遞過來用戶名密碼,認證服務器會返回一個token,其后用戶請求訪問資源時帶上這個token,即可訪問其有權限的資源。詳細參考:https://developer.okta.com/blog/2018/06/29/what-is-the-oauth2-password-grant 。

所以本文中所指單點登錄就是獲取這個token的過程。

token獲取的鏈接是:/oauth/token ,對應代碼在spring-security-oauth2 jar包的 org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken ;

如果想了解整個流程走過的過濾器,可以在org.springframework.security.web.FilterChainProxy中斷點跟蹤執行到的過濾器鏈中的過濾器,具體可以在org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter方法內斷點

這里我主要想了解token的生成,所以直接在postAccessToken方法內斷點,一步步走,直到如下這行代碼,

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

這里就是請求授予token的地方,一步步斷點跟進去,最后回到方法org.springframework.security.oauth2.provider.CompositeTokenGranter#grant,

1 public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
2         for (TokenGranter granter : tokenGranters) {
3             OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
4             if (grant!=null) {
5                 return grant;
6             }
7         }
8         return null;
9     }

這里的tokenRequest見下圖,

從字面就看大概看出這對應oauth2的5種grant types,grant types參考https://oauth.net/2/grant-types/ ,我們密碼模式對應上圖中的最后一個Granter,方法org.springframework.security.oauth2.provider.CompositeTokenGranter#grant的第三行不同Granter都是根據grant type來選擇適用Granter,如果不匹配直接返回null。進入最后一個Granter的grant方法如下,

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

 

最后進入getOAuth2Authentication(client, tokenRequest),方法內有如下代碼,

userAuth = authenticationManager.authenticate(userAuth);

上面authenticationManager是一個org.springframework.security.authentication.ProviderManager,封裝了若干org.springframework.security.authentication.AuthenticationProvider,provider進行認證並返回非空Authentication則結束繼續認證,我們項目中使用的是org.springframework.security.authentication.dao.DaoAuthenticationProvider,她會做各種check(比如配置文件中配置的Checker),用戶密碼校驗等;

接着進入org.springframework.security.oauth2.provider.token.DefaultTokenServices#createAccessToken(org.springframework.security.oauth2.provider.OAuth2Authentication)方法,

 1 @Transactional
 2     public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
 3 
 4         OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
 5         OAuth2RefreshToken refreshToken = null;
 6         if (existingAccessToken != null) {
 7             if (existingAccessToken.isExpired()) {
 8                 if (existingAccessToken.getRefreshToken() != null) {
 9                     refreshToken = existingAccessToken.getRefreshToken();
10                     // The token store could remove the refresh token when the
11                     // access token is removed, but we want to
12                     // be sure...
13                     tokenStore.removeRefreshToken(refreshToken);
14                 }
15                 tokenStore.removeAccessToken(existingAccessToken);
16             }
17             else {
18                 // Re-store the access token in case the authentication has changed
19                 tokenStore.storeAccessToken(existingAccessToken, authentication);
20                 return existingAccessToken;
21             }
22         }
23 
24         // Only create a new refresh token if there wasn't an existing one
25         // associated with an expired access token.
26         // Clients might be holding existing refresh tokens, so we re-use it in
27         // the case that the old access token
28         // expired.
29         if (refreshToken == null) {
30             refreshToken = createRefreshToken(authentication);
31         }
32         // But the refresh token itself might need to be re-issued if it has
33         // expired.
34         else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
35             ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
36             if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
37                 refreshToken = createRefreshToken(authentication);
38             }
39         }
40 
41         OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
42         tokenStore.storeAccessToken(accessToken, authentication);
43         // In case it was modified
44         refreshToken = accessToken.getRefreshToken();
45         if (refreshToken != null) {
46             tokenStore.storeRefreshToken(refreshToken, authentication);
47         }
48         return accessToken;
49 
50     }

我們項目使用redis存儲token的,如果token存在且未過期,20行直接返回,后面的代碼則是關於refreshToken是否過期,過期刷新,accessToken和refreshToken創建的過程,默認都是UUID.randomUUID().toString()生成一個UUID,都有過期時間機制,最后存入
tokenStore,對於我們的項目tokenStore就是redis。

到此token獲取流程分析完畢。




三、請求的認證與授權流程分析

客戶請求某個子應用時,需要先到認證服務器去認證,接着到授權服務器去檢測權限,那么如何辦到的呢? 假設服務應用為A,我們這里認證和授權的兩個服務是做在一期的,姑且稱之為B;

認證與授權的流程簡圖如下,

圖1 

 
        

 圖2

圖3

 

 

圖2和圖3分別是服務模塊和認證授權模塊中的過濾器鏈;抓住過濾器鏈方才能夠掌握整個流程。

 

 

 
        
 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM