Spring Security 是一個多模塊的項目,以前梳理了一下 Spring Security 認證流程,如今才發現,梳理的那部份內容更多的只是 Spring Security Core 這個核心模塊中的內容。html
平常使用時,還會更多的涉及 Spring Security Web 和 Spring Security OAuth2 中的東西,這篇博客的主要內容即是梳理一下這三者之間的關係,瞭解一下各自發揮的做用。java
Spring Security Core 在整個 Spring Security 框架中扮演着重要的角色,提供了有關於認證和權限控制相關的抽象。git
然而,在使用的過程當中,咱們接觸的更多的多是和認證相關的抽象,好比:github
AuthenticationManager
提供了進行用戶認證方法的抽象,容許經過 ProviderManager
和 AuthenticationProvider
來組裝和實現本身的認證方法UserDetails
和 UserDetailsService
提供了用戶詳細信息和獲取用戶詳細信息方式的抽象Authentication
提供了用戶認證信息和認證結果的抽象SecurityContext
和 SecurityContextHolder
提供了保存認證結果的方式這些東西其實就是將傳統的認證流程中的關鍵組成單獨抽象了出來,結合傳統的認證流程能夠很容易的理解這些組件之間的關係,也能夠看這張來自 Spring Security(一) —— Architecture Overview | 芋道源碼 —— 純源碼解析博客 的一張圖片:web
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7573921?w=995&h=562&f=png&s=45816">spring
而權限控制部分的抽象,主要就是 AccessDecisionManager
和 AccessDecisionVoter
了,這兩個東西我目前尚未手動操做過,只能說,Spring Security Web 提供的服務太貼心, 權限控制部分的實現並不須要我操太多心。安全
關於 Spring Security Core 模塊更多的內容能夠參考:服務器
若是說 Spring Security Core 只是提供了認證和權限控制相關的抽象的話,Spring Security Web 便爲咱們提供了這些抽象的具體實現與應用。session
Spring Security Web 經過 過濾器鏈 來實現了和 Web 安全相關的一系列功能,而用戶的認證和權限控制只是其中的一部分,在這部分的實現中,過濾器充當 Spring Security Core 調用者的身份,通常流程爲:架構
Authentication
傳遞給 AuthenticationManager
進行認證,而後將認證結果放到 SecurityContext
中供後續過濾器使用AccessDecisionManager
判斷是否具有相應的權限在這裏,Spring Security Core 只是 Spring Security Web 利用的一部分功能,更爲重要的是,整個過濾器鏈。
以前原本只是想了解一下過濾器鏈的調用過程,可是看着看着,就跑到源碼去了。反應過來的時候才發現,已經搞了這麼多了停下來的話有點吃虧,就乾脆把過濾器鏈的構建邏輯理了一下。
<details><summary><i></i></summary>
在梳理完構建器鏈的構建和調用邏輯後感受,過濾器鏈的構建邏輯貌似沒有好多用,還不如直接看過濾器鏈的調用邏輯……
</details>
這部分邏輯的梳理過程有些複雜,反正我調試的時候斷點就在 build()
方法附近反覆橫跳,這裏爲了簡單,就直接放結果了<sup><a id="fnr.1" class="footref" href="#fn.1">1</a></sup>:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58c73711fa5?w=1125&h=777&f=png&s=86478">
時序圖畫的不是很標準,大體意思一下就能夠了哈( ̄▽ ̄),解析以下:
WebSecurity
和 HttpSecurity
完成的WebSecurity
根據上下文中的 WebSecurityConfigurer
構建出 HttpSecurity
對象,而後經過 HttpSecurity
構建出 SecurityFilterChain
後,將 SecurityFilterChain
放到 FilterChainProxy
中。 其中,WebSecurityConfigurer 的經常使用實現爲 WebMvcConfigurerAdapter
, 而 SecurityFilterChain
的經常使用實現爲 DefaultSecurityFilterChain
HttpSecurity
根據直接添加的 Filter
和經過 AbstractHttpConfigurer
實現類構建的 Filter
生成過濾器鏈這部分邏輯中,關鍵的對象分別是 WebSecurity
和它依賴的配置類 WebSecurityConfigurer
, HttpSecurity
和它依賴的配置類 AbstractHttpConfigurer
.
在實際的使用中,咱們一般會繼承 WebMvcConfigurerAdapter
這個 WebSecurityConfigurer
的實現類,而後在重寫它的 configure(HttpSecurity)
方法:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests() .antMatchers("/oauth/**") .authenticated() .and() .requestMatchers() .antMatchers("/oauth/**","/login/**","/logout/**") .and() .csrf() .disable() .formLogin() .permitAll(); // @formatter:on } }
在上面這個類中,咱們繼承了 WebSecurityConfigurerAdapter
這個類,當咱們將自定義的類放到 Spring 上下文中後,就能夠被 WebSecurity 拿到用於構建 HttpSecurity, 而重寫的 configure(HttpSecurity)
則會在 HttpSecurity 構建過濾器以前調用,完成過濾器鏈的配置。
其中,諸如 csrf()
之類的方法都會返回一個 AbstractHttpConfigurer
實現,容許咱們對特定的過濾器進行配置。
到了最後,HttpSecurity 就能夠根據相應的配置完成過濾器鏈的構建,而後再由 WebSecurity 將它們放到 FilterChainProxy
實例中返回。
過濾器鏈的調用的話,主要涉及兩個對象:FilterChainProxy 和 DefaultSecurityFilterChain,關鍵其實仍是在 FilterChainProxy 上。
然而,這兩個對象的源碼都挺簡單的,這裏就不貼了,有興趣的能夠去看一下,這裏簡單說一下結果:
這裏的關鍵點其實就是,存在多條過濾器鏈,每條過濾器鏈匹配必定的請求。以前看文檔的時候不仔細,沒有意識到這一點,饒了很多彎路 QAQ
附圖:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7967f3e?w=1190&h=839&f=png&s=65564">
Spring Security Web 過濾器的使用主要就是自定義過濾器鏈,默認的過濾器鏈會添加一些 Spring Security Web 自帶的一些過濾器,使用時,須要考慮是否去掉默認的一些過濾器器(或者不使用默認配置), 並將自定義的過濾器添加到過濾器鏈中的一個合適的位置上。
這裏會簡要介紹部份內置過濾器的做用和過濾器的順序,首先是內置的幾個過濾器:
SecurityContextPersistenceFilter
能夠從 Session 中取出已認證用戶的信息AnonymousAuthenticationFilter
在發現 SecurityContextHolder
中尚未認證信息時,會生成一個匿名認證信息放到 SecurityContextHolder
ExceptionTranslationFilter
能夠處理 FilterSecurityInterceptor
中拋出的異常,進行重定向、輸出錯誤信息等FilterSecurityInterceptor
對認證信息的權限進行判斷,權限不足時拋出異常在自定義過濾器時(一般是認證過濾器),咱們須要考慮自定義過濾器的位置,好比,咱們不該該把自定義的認證過濾器放在 AnonymousAuthenticationFilter
的後面,官方文檔對過濾器的順序給出瞭解釋: 在去除一些過濾器後,大體順序就爲:
<img src="https://user-gold-cdn.xitu.io/2019/11/3/16e2f52abed85b6f?w=247&h=269&f=png&s=17760">
其中,AuthenticationProcessingFilter 是指認證過濾器實現,好比經常使用的 UsernamePasswordAuthenticationFilter
這個過濾器。
完整的順序能夠參考:
在這個順序中,因爲 SecurityContextPersistenceFilter
可能從 Session 中取出已認證用戶的信息,所以,自定義過濾器時應該考慮 SecurityContextHolder 是否是已經存在用戶認證信息。 或者在登陸/註冊相關 URL 的過濾器鏈中設置認證用戶帳戶密碼的過濾器,在其餘過濾器鏈中設置認證 token 的過濾器。
Spring Security OAuth2 創建在 Spring Security Core 和 Spring Security Web 的基礎上,提供了對 OAuth2 受權框架的支持。
其中,最爲複雜的部分是在 受權服務器 上,相對的,資源服務器基本上就是重用 Spring Security Web 提供的過濾器鏈,經過過濾器 OAuth2AuthenticationProcessingFilter
和請求攜帶的 Token
獲取認證信息, 所以,這裏的重心會放在受權服務器上。
對於傳統的認證方式來講,簡單認證用戶的信息基本上就足夠了,可是對於 OAuth2 來講是不夠的,對於 OAuth2 受權服務器來講,除了須要完成用戶的認證之外,還需完成客戶端的認證,還須要效驗客戶端請求的 Scope, 所以,單憑過濾器鏈是不足以完成二者的認證的,由於 SecurityContextHolder 只能持有一個認證結果。
因而,Spring Security OAuth2 採用的認證策略即是:在過濾器鏈中完成客戶端或用戶的認證,而後再在端點的內部邏輯中完成剩餘信息的效驗。而這個認證策略,在不一樣模式中也是不同的。
這裏主要會對 受權碼模式 和 密碼模式 中的認證策略進行介紹,由於這兩個模式中使用到的端點 AuthorizationEndpoint
和 TokenEndpoint
已經涵蓋了兩條主要的過濾器鏈。
首先是受權碼模式,對於受權碼模式來講,請求流程一般是先到 /oauth/authorize
獲取受權碼,而後再到 /oauth/token
獲取 Token,對於 /oauth/authorize
這個端點的過濾器鏈來講, 認證的是用戶的信息,認證經過後進入端點內部,會對客戶端請求 Scope
和用戶的 Approval
進行效驗,效驗經過會生成受權碼返回給客戶端。
其實這裏也就能夠明白爲何 /oauth/authorize
這個端點須要對用戶進行認證了,由於,這裏須要獲取的是 用戶 的受權。
而後客戶端拿着受權碼去 /oauth/token
這個端點獲取 Token 時,該端點的過濾器鏈會對客戶端進行認證,認證經過後進入端點內部,這時端點內部會對客戶端請求的 Scope 進行效驗, 效驗經過後就會經過 TokenGranter
生成 Token 返回給客戶端。
也就是說,對於受權碼模式來講:
/oauth/authorize
完成用戶的認證、客戶端請求的 Scope 的效驗、用戶的受權檢查/oauth/token
完成客戶端的認證,客戶端請求的 Scope 的效驗、客戶端受權碼的檢查這其實就能夠看作時對受權碼模式的代碼解釋,由於,在受權碼模式中,去獲取 Token 的每每不是用戶操做的客戶端,所以,須要認證客戶端是不是受信任的。
相關邏輯對應的源碼,去掉了一部分效驗代碼:
@RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) { AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters); try { // 未經過認證的請求會拋異常 if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) { throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed."); } ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId()); // 效驗 Scope oauth2RequestValidator.validateScope(authorizationRequest, client); // 效驗用戶的受權 authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); // Validation is all done, so we can check for auto approval... if (authorizationRequest.isApproved()) { if (responseTypes.contains("token")) { return getImplicitGrantResponse(authorizationRequest); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal)); } } return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal); } catch (RuntimeException e) { sessionStatus.setComplete(); throw e; } } @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { // 能夠看到,經過效驗的是客戶端 String clientId = getClientId(principal); ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); // 效驗請求的 Scope if (authenticatedClient != null) { oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } if (isAuthCodeRequest(parameters)) { // The scope was requested or determined during the authorization step if (!tokenRequest.getScope().isEmpty()) { tokenRequest.setScope(Collections.<String> emptySet()); } } // 調用 TokenGranter 進行受權 OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } return getResponse(token); }
受權碼模式流程圖:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58c6bb340ca?w=710&h=406&f=png&s=30973">
密碼模式,或者說簡化模式,只有一個端點即 /oauth/token
這個端點,也就是說,這個端點要同時完成用戶和客戶端的認證。
可是,這個端點不可能同時擁有兩個過濾器鏈,而爲了支持受權碼模式,這個端點的過濾器鏈的職責已經肯定了,就是完成客戶端的認證。所以,用戶的認證就只能在端點內部邏輯完成。
當 TokenEndpoint
發現受權模式爲 密碼模式 時,會將 ResourceOwnerPasswordTokenGranter
放入 TokenGranter
, 而 ResourceOwnerPasswordTokenGranter
進行受權時會調用 AuthenticationManager
來完成對用戶的認證,認證成功纔會經過 TokenService
生成 Token 返回。
// AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters private List<TokenGranter> getDefaultTokenGranters() { List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>(); tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory)); tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory)); tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory)); tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory)); if (authenticationManager != null) { tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetails, requestFactory)); } return tokenGranters; }
密碼模式流程圖:
<img src="https://user-gold-cdn.xitu.io/2019/11/2/16e2c58cc7efb8fe?w=700&h=504&f=png&s=35288">
經過對 受權碼模式 和 密碼模式 的瞭解咱們知道了客戶端的認證是在過濾器鏈中完成的,這個認證能夠經過 BasicAuthenticationFilter
完成,但更通用的大概是 ClientCredentialsTokenEndpointFilter
這個過濾器。
其內部的認證流程實際上是很簡單的,最爲重要的一點是,它用的仍是 Spring Security Core 那一套!
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String clientId = request.getParameter("client_id"); String clientSecret = request.getParameter("client_secret"); // If the request is already authenticated we can assume that this filter is not needed Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { return authentication; } UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret); // 經過 AuthenticationManager 完成認證 return this.getAuthenticationManager().authenticate(authRequest); }
咱們知道,Spring Security OAuth2 提供了 ClientDetails 和 ClientDetailsService 這兩種抽象,它們和 UserDetails 和 UserDetailsService 是不兼容的,這時,能夠選擇本身實現一個 AuthenticationProvider 使用 ClientDetails 和 ClientDetailsService, 但也能夠將 ClientDetails 和 ClientDetailsService 轉換爲 UserDetails 和 UserDetailsService,Spring Security OAuth2 經過 ClientDetailsUserDetailsService 來完成這一轉換:
public class ClientDetailsUserDetailsService implements UserDetailsService { private final ClientDetailsService clientDetailsService; public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) { this.clientDetailsService = clientDetailsService; } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { ClientDetails clientDetails; try { clientDetails = clientDetailsService.loadClientByClientId(username); } catch (NoSuchClientException e) { throw new UsernameNotFoundException(e.getMessage(), e); } String clientSecret = clientDetails.getClientSecret(); if (clientSecret== null || clientSecret.trim().length()==0) { clientSecret = emptyPassword; } return new User(username, clientSecret, clientDetails.getAuthorities()); } }
Spring Security OAuth2 中受權碼的生成時經過 TokenGranter 來完成的,進行受權碼的生成時,會遍歷擁有的各個 TokenGranter 實現,直到成功生成 Token 或者全部 TokenGranter 實現都不能生成 Token。
生成 Token 也是一個能夠抽象出來的環節,所以,Spring Security OAuth2 經過 TokenService 和 TokenStore 來生成、獲取和保存 Token。
public abstract class AbstractTokenGranter implements TokenGranter { private final AuthorizationServerTokenServices tokenServices; private final ClientDetailsService clientDetailsService; private final OAuth2RequestFactory requestFactory; private final String grantType; protected AbstractTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) { this.clientDetailsService = clientDetailsService; this.grantType = grantType; this.tokenServices = tokenServices; this.requestFactory = requestFactory; } public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { // 每一個 TokenGranter 對應一種受權類型 if (!this.grantType.equals(grantType)) { return null; } String clientId = tokenRequest.getClientId(); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); validateGrantType(grantType, client); // 獲取受權碼 return getAccessToken(client, tokenRequest); } protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); } } // 默認的 TokenServices 的部分代碼 public class DefaultTokenServices { @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { // 首先從 TokenStore 中獲取 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 { // Re-store the access token in case the authentication has changed tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; } } if (refreshToken == null) { refreshToken = createRefreshToken(authentication); } OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); // 保存 accessToken tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; } // 從 TokenStore 中獲取 Token public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { return tokenStore.getAccessToken(authentication); } }
簡單來講就是:
資源服務器相較於受權服務器來講就要簡單多了,和傳統的流程差很少,經過過濾器 OAuth2AuthenticationProcessingFilter
和 OAuth2AuthenticationManager
驗證 Token 並獲取認證信息:
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; // 從請求頭中提取 Token Authentication authentication = tokenExtractor.extract(request); Authentication authResult = authenticationManager.authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); } } public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String token = (String) authentication.getPrincipal(); // 經過 TokenService 獲取認證信息 OAuth2Authentication auth = tokenServices.loadAuthentication(token); if (auth == null) { throw new InvalidTokenException("Invalid token: " + token); } checkClientDetails(auth); if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); // Guard against a cached copy of the same details if (!details.equals(auth.getDetails())) { // Preserve the authentication details from the one loaded by token services details.setDecodedDetails(auth.getDetails()); } } auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; } }
不少地方均可以看到 JWT 在 OAuth2 中的使用,Spring Security JWT 在 Spring Security OAuth2 中便扮演了 TokenService 和 TokenStore 的角色,用於生成和效驗 Token。
可是,我仍是很想吐槽一下 JWT 這個東西。當初剛看到的時候感受頗有趣,使用 JWT 能夠直接在 Token 中攜帶一些信息,同時服務端還不用存儲 Token 的信息。
然而,在實際的一些使用中,可能會碰見須要做廢還有效的 JWT Token 的需求,這對於 JWT 來講是沒法實現的。爲了實現這一需求,就只能在服務端存儲一些信息。
可是,既然都要在服務端存儲信息了,那幹嗎還用 JWT 呢?只要須要在服務端存儲信息,那麼,用不用 JWT 都沒多大區別了啊……
Spring Security 真的是一個很複雜的框架,目前設計的還只是在 Servlet 程序中的應用,然鵝我目前忽然對 Spring WebFlux 產生了一點興趣, 不知道 Spring Security 在 Spring WebFlux 中是啥樣的……
另外,我想說的是,Spring Security 的官方教程真的很棒,將大致的架構都解釋清楚了,惋惜吃了英語的虧 T<sub>T</sub>
Spring Security 總體相關的資料:
Spring Security Web 相關的資料:
Spring Security OAuth2 相關的資料:
<sup><a id="fn.1" href="#fnr.1">1</a></sup> 對詳細過程有興趣的,能夠看個人筆記 Spring Security Web 過濾器鏈的構建