系統架構流程

服務模塊可以有多個,認證和授權是做在一起的單獨一個模塊。
原本想寫關於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分別是服務模塊和認證授權模塊中的過濾器鏈;抓住過濾器鏈方才能夠掌握整個流程。
