1. 程式人生 > >Spring Security如何優雅的增加OAuth2協議授權模式

Spring Security如何優雅的增加OAuth2協議授權模式

![file](https://img2020.cnblogs.com/other/1769816/202009/1769816-20200902102500038-900353640.png) ## 一、什麼是OAuth2協議? OAuth 2.0 是一個關於授權的開放的網路協議,是目前最流行的授權機制。 資料的所有者告訴系統,同意授權第三方應用進入系統,獲取這些資料。系統從而產生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。 由於授權的場景眾多,OAuth 2.0 協議定義了獲取令牌的四種授權方式,分別是: * **授權碼模式**:授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與"服務提供商"的認證伺服器進行互動。 * **簡化模式**:簡化模式(implicit grant type)不通過第三方應用程式的伺服器,直接在瀏覽器中向認證伺服器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。 * **密碼模式**:密碼模式(Resource Owner Password Credentials Grant)中,使用者向客戶端提供自己的使用者名稱和密碼。客戶端使用這些資訊,向"服務商提供商"索要授權。 * **客戶端模式**:客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以使用者的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,使用者直接向客戶端註冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。 >四種授權模式分別使用不同的 `grant_type` 來區分   ## 二、為什麼要自定義授權型別? 雖然 OAuth2 協議定義了4種標準的授權模式,但是在實際開發過程中還是遠遠滿足不了各種變態的業務場景,需要我們去擴充套件。 > 例如增加圖形驗證碼、手機驗證碼、手機號密碼登入等等的場景   而常見的做法都是通過增加 `過濾器Filter` 的方式來擴充套件 `Spring Security` 授權,但是這樣的實現方式有兩個問題: 1. 脫離了 `OAuth2` 的管理 2. 不靈活:例如系統使用 **密碼模式** 授權,網頁版需要增加圖形驗證碼校驗,但是手機端APP又不需要的情況下,使用增加 `Filter` 的方式去實現就比較麻煩了。   所以目前在 `Spring Security` 中比較優雅和靈活的擴充套件方式就是通過自定義 **grant_type** 來增加授權模式。   ## 三、實現思路 在擴充套件之前首先需要先了解 `Spring Security` 的整個授權流程,我以 **密碼模式** 為例去展開分析,如下圖所示 ![file](https://img2020.cnblogs.com/other/1769816/202009/1769816-20200902102500584-1408695399.png) ### 3.1. 流程分析 整個授權流程關鍵點分為以下兩個部分: **第一部分**:關於授權型別 `grant_type` 的解析 1. 每種 `grant_type` 都會有一個對應的 `TokenGranter` 實現類。 2. 所有 `TokenGranter` 實現類都通過 `CompositeTokenGranter` 中的 `tokenGranters` 集合存起來。 3. 然後通過判斷 `grantType` 引數來定位具體使用那個 `TokenGranter` 實現類來處理授權。   **第二部分**:關於授權登入邏輯 1. 每種 `授權方式` 都會有一個對應的 `AuthenticationProvider` 實現類來實現。 2. 所有 `AuthenticationProvider` 實現類都通過 `ProviderManager` 中的 `providers` 集合存起來。 3. `TokenGranter` 類會 new 一個 `AuthenticationToken` 實現類,如 `UsernamePasswordAuthenticationToken` 傳給 `ProviderManager` 類。 4. 而 `ProviderManager` 則通過 `AuthenticationToken` 來判斷具體使用那個 `AuthenticationProvider` 實現類來處理授權。 5. 具體的登入邏輯由 `AuthenticationProvider` 實現類來實現,如 `DaoAuthenticationProvider`。   ### 3.2. 擴充套件分析 根據上面的流程,擴充套件分為以下兩種場景 **場景一**:只對原有的授權邏輯進行增強或者擴充套件,如:使用者名稱密碼登入前增加圖形驗證碼校驗。 該場景需要定義一個新的 `grantType` 型別,並新增對應的 `TokenGranter` 實現類 **新增擴充套件內容**,然後加到 `CompositeTokenGranter` 中的 `tokenGranters` 集合裡即可。 > 參考程式碼:[PwdImgCodeGranter.java](https://gitee.com/zlt2000/microservices-platform/blob/master/zlt-uaa/src/main/java/com/central/oauth/granter/PwdImgCodeGranter.java)   **場景二**:新加一種授權方式,如:手機號加密碼登入。 該場景需要實現以下內容: 1. 定義一個新的 `grantType` 型別,並新增對應的 `TokenGranter` 實現類新增到 `CompositeTokenGranter` 中的 `tokenGranters` 集合裡 2. 新增一個 `AuthenticationToken` 實現類,用於存放該授權所需的資訊。 3. 新增一個 `AuthenticationProvider` 實現類 **實現授權的邏輯**,並重寫 `supports` 方法繫結步驟二的 `AuthenticationToken` 實現類 > 參考程式碼:[MobilePwdGranter.java](https://gitee.com/zlt2000/microservices-platform/blob/master/zlt-uaa/src/main/java/com/central/oauth/granter/MobilePwdGranter.java)   ## 四、程式碼實現 下面以 **場景二** 新增手機號加密碼授權方式為例,展示核心的程式碼實現 ### 4.1. 建立 AuthenticationToken 實現類 建立 `MobileAuthenticationToken` 類,用於儲存手機號和密碼資訊 ```java public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; private Object credentials; public MobileAuthenticationToken(String mobile, String password) { super(null); this.principal = mobile; this.credentials = password; setAuthenticated(false); } public MobileAuthenticationToken(Object principal, Object credentials, Collection authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } } ```   ### 4.2. 建立 AuthenticationProvider 實現類 建立 `MobileAuthenticationProvider` 類,實現登入邏輯,並繫結 `MobileAuthenticationToken` 類 ```java @Setter public class MobileAuthenticationProvider implements AuthenticationProvider { private ZltUserDetailsService userDetailsService; private PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) { MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); String password = (String) authenticationToken.getCredentials(); UserDetails user = userDetailsService.loadUserByMobile(mobile); if (user == null) { throw new InternalAuthenticationServiceException("手機號或密碼錯誤"); } if (!passwordEncoder.matches(password, user.getPassword())) { throw new BadCredentialsException("手機號或密碼錯誤"); } MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(user, password, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class authentication) { return MobileAuthenticationToken.class.isAssignableFrom(authentication); } } ```   ### 4.3. 建立 TokenGranter 實現類 建立 `MobilePwdGranter` 類並定義 `grant_type` 的值為 `mobile_password` ```java public class MobilePwdGranter extends AbstractTokenGranter { private static final String GRANT_TYPE = "mobile_password"; private final AuthenticationManager authenticationManager; public MobilePwdGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices , ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); this.authenticationManager = authenticationManager; } @Override protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {