Spring security OAuth2 深入解析

CatalpaFlat發表於2017-12-22

Spring security OAuth2 深入解析

一、OAuth2 概要

1.1.OAuth2基本流程

話不多說,先上圖:

OAuth2流程圖

分析一波:

  • client:第三方應用(即App或向外提供介面)
  • Resource Owner:資源所有者(即使用者)
  • Authentication Server:授權認證服務(發配Access Token)
  • Resource Server:資源伺服器(儲存使用者資源資訊等資源)

其實不管微信或者QQ大體上都是使用這種OAuth2的基本流程:

  1. 第三方應用請求使用者授權;
  2. 使用者同意授權,並返回一個授權碼(code);
  3. 第三方應用根據授權碼(code)向授權認證服務進行授權;
  4. 授權伺服器根據授權碼(code),校驗通過,並返回給第三方應用令牌(Access Token);
  5. 第三方應用根據令牌(Access Token)向資源服務請求相關資源;
  6. 資源伺服器驗證令牌(Access Token),校驗通過,並返回第三方所請求的資源。

1.2.服務型別

OAuth2 在服務提供者上可分為兩類:

  • 授權認證服務:AuthenticationServer

@Configuration
@EnableAuthorizationServer
public class CustomAuthenticationServerConfig extends AuthorizationServerConfigurerAdapter 
複製程式碼
  • 資源獲取服務:ResourceServer

@Configuration
@EnableResourceServer
public class CustomResourceServerConfig extends ResourceServerConfigurerAdapter
複製程式碼

注:這兩者有時候可能存在同一個應用程式中(即SOA架構)。在Spring OAuth中可以簡便的將其分配到兩個應用中(即微服務),而且可多個資源獲取服務共享一個授權認證服務。

1.3.授權認證服務

主要的操作:

  1. 獲取第三方應用傳送的授權碼(code)以及第三方應用標識
  2. 根據授權碼及標識進行校驗
  3. 校驗通過,傳送令牌(Access Token)

分析一波:
1)第一步操作

  • 授權碼(code):第三方應用進行第一步“Authorization Request”時,請求引數redirect_uri中的回撥連結,服務會生成相關使用者憑證,並在其回撥連結上附帶code
  • 第三方使用者標識
    • client_id: 第三方使用者的id(可理解為賬號)
    • client_secret:第三方應用和授權伺服器之間的安全憑證(可理解為密碼)

注:其中client_id和client_secret都是授權伺服器傳送給第三方應用的,如:微信等一系列授權,在其平臺上註冊,獲取其appid和secret同樣道理(個人理解為賬號密碼)。

既然是賬號祕密,總不能以get請求,也太不安全了。因此,OAuth2要求該請求必須是POST請求,同時,還必須時HTTPS服務,以此保證獲取到的安全憑證(Access Token)的安全性。

2)第二步操作

  • 授權認證伺服器根據標識校驗第三方應用的真實性
  • 授權認證伺服器根據授權碼(code)進行校驗使用者憑證

3)第三步操作

  • 生成Access Token(MD5型別,uuid型別,jwt型別等)

1.4.資源獲取服務

主要的操作:

  • 校驗Access Token
  • 發放資源資訊

二、Spring Security OAuth2的使用

1.授權認證服務

spring OAuth2中,我們配置一個授權認證服務,我們最主要有以下三點:

  1. 第三方使用者客戶端詳情 → Client
  2. 令牌的生成管理 → Access Token
  3. 端點接入 → endpoints

spring中有三個配置與這三點一一對應:

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全約束.
  • AuthorizationServerEndpointsConfigurer:來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)

1.1.第三方使用者客戶端詳情

除了上面說到的client_id和client_secret,還需要一些服務附帶一些授權認證引數。

1).Grant Type

其實OAuth2不僅提供授權碼(code)這種格式授權方式,還提供幾個其他型別。其中用Grant Type代表當前授權的型別。 Grant Type包括:

  • authorization_code:傳統的授權碼模式
  • implicit:隱式授權模式
  • password:資源所有者(即使用者)密碼模式
  • client_credentials:客戶端憑據(客戶端ID以及Key)模式
  • refresh_token:獲取access token時附帶的用於重新整理新的token模式

2).scope

其實授權賦予第三方使用者可以在資源伺服器獲取資源,經常就是調取Api請求附帶令牌,然而調取api有增刪查改等功能,而scopes的值就是all(全部許可權),read,write等許可權。就是第三方訪問資源的一個許可權,訪問範圍。


3).accessTokenValiditySeconds

還可以設定accessTokenValiditySeconds屬性來設定Access Token的存活時間。

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

    clients.inMemory()
            .withClient("catalpaFlat")
            .secret("catalpaFlat-secret")
            .accessTokenValiditySeconds(7200)
            .authorizedGrantTypes("refresh_token","password")
            .scopes("all");
}
複製程式碼

1.2.令牌的生成和管理

AccessToken的存在意義:

  • 建立AccessToken,並儲存,以備後續請求訪問都可以認證成功並獲取到資源
  • AccessToken還有一個潛在功能,就是使用jwt生成token時候,可以用來載入一些資訊,把一些相關許可權等包含在AccessToken中

1).AuthorizationServerTokenServices
AuthorizationServerTokenServices 提供了對AccessToken的相關操作建立、重新整理、獲取。

public interface AuthorizationServerTokenServices {

	OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

	
	OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
			throws AuthenticationException;

	
	OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}
複製程式碼

2).DefaultTokenServices

AuthorizationServerTokenServices竟然可以操作AccessToken,那麼OAuth2就預設為我們提供了一個預設的DefaultTokenServices。包含了一些有用實現,可以使用它來修改令牌的格式和令牌的儲存等,但是生成的token是隨機數。

3).TokenStore

建立AccessToken完之後,除了發放給第三方,肯定還得儲存起來,才可以使用。因此,TokenStore為我們完成這一操作,將令牌(AccessToken)儲存或持久化。
TokenStore也有一個預設的實現類InMemoryTokenStore,從名字就知道是通過儲存到記憶體進而實現儲存Access Token。 TokenStore的實現有多種型別,可以根據業務需求更改Access Token的儲存型別:

  • InMemoryTokenStore:這個是OAuth2預設採用的實現方式。在單服務上可以體現出很好特效(即併發量不大,並且它在失敗的時候不會進行備份),大多專案都可以採用此方法。畢竟存在記憶體,而不是磁碟中,除錯簡易。
  • JdbcTokenStore:這個是基於JDBC的實現,令牌(Access Token)會儲存到資料庫。這個方式,可以在多個服務之間實現令牌共享。
  • JwtTokenStore:jwt全稱 JSON Web Token。這個實現方式不用管如何進行儲存(記憶體或磁碟),因為它可以把相關資訊資料編碼存放在令牌裡。JwtTokenStore 不會儲存任何資料,但是它在轉換令牌值以及授權資訊方面與 DefaultTokenServices 所扮演的角色是一樣的。但有兩個缺點:
    • 撤銷一個已經授權的令牌會很困難,因此只適用於處理一個生命週期較短的以及撤銷重新整理令牌。
    • 令牌佔用空間大,如果加入太多使用者憑證資訊,會存在傳輸冗餘

4).JWT Token

想使用jwt令牌,需要在授權服務中配置JwtTokenStore。之前說了,jwt將一些資訊資料編碼後存放在令牌,那麼其實在傳輸的時候是很不安全的,所以Spring OAuth2提供了JwtAccessTokenConverter來懟令牌進行編碼和解碼。適用JwtAccessTokenConverter可以自定義祕籤(SigningKey)。SigningKey用處就是在授權認證伺服器生成進行簽名編碼,在資源獲取伺服器根據SigningKey解碼校驗。

JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();

 jwtAccessTokenConverter.setSigningKey("CatalpaFlat")
複製程式碼

jwt儲存

1.3.端點接入-endpoints

授權認證是使用AuthorizationEndpoint這個端點來進行控制,一般使用AuthorizationServerEndpointsConfigurer 來進行配置。

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
複製程式碼

1).端點(endpoints)的相關屬性配置:

  • authenticationManager:認證管理器。若我們上面的Grant Type設定為password,則需設定一個AuthenticationManager物件
  • userDetailsService:若是我們實現了UserDetailsService,來管理使用者資訊,那麼得設我們的userDetailsService物件
  • authorizationCodeServices:授權碼服務。若我們上面的Grant Type設定為authorization_code,那麼得設一個AuthorizationCodeServices物件
  • tokenStore:這個就是我們上面說到,把我們想要是實現的Access Token型別設定
  • accessTokenConverter:Access Token的編碼器。也就是JwtAccessTokenConverter
  • tokenEnhancer:token的擴充。當使用jwt時候,可以實現TokenEnhancer來進行jwt對包含資訊的擴充
  • tokenGranter:當預設的Grant Type已經不夠我們業務邏輯,實現TokenGranter 介面,授權將會由我們控制,並且忽略Grant Type的幾個屬性。

2).端點(endpoints)的授權url: 要授權認證,肯定得由url請求,才可以傳輸。因此OAuth2提供了配置授權端點的URL。
AuthorizationServerEndpointsConfigurer ,還是這個配置物件進行配置,其中由一個pathMapping()方法進行配置授權端點URL路徑,預設提供了兩個引數defaultPath和customPath:

public AuthorizationServerEndpointsConfigurer pathMapping(String defaultPath, String customPath) {
		this.patternMap.put(defaultPath, customPath);
		return this;
}
複製程式碼

pathMapping的defaultPath有:

  • /oauth/authorize:授權端點
  • /oauth/token:令牌端點
  • /oauth/confirm_access:使用者確認授權提交端點
  • /oauth/error:授權服務錯誤資訊端點
  • /oauth/check_token:用於資源服務訪問的令牌解析端點
  • /oauth/token_key:提供公有密匙的端點,如果使用JWT令牌的話

注:pathMapping的兩個引數都將以 "/" 字元為開始的字串

1.4.自定義錯誤處理(Error Handling)

實際上我們上面說到的端點,其實可以看成Controller,用於返回不同端點的響應內容。

授權服務的錯誤資訊是使用標準的Spring MVC來進行處理的,也就是 @ExceptionHandler 註解的端點方法,我們可以提供一個 WebResponseExceptionTranslator 物件。最好的方式是改變響應的內容而不是直接進行渲染。

  • 假如說在呈現令牌端點的時候發生了異常,那麼異常委託了 HttpMessageConverters 物件(它能夠被新增到MVC配置中)來進行輸出。
  • 假如說在呈現授權端點的時候未通過驗證,則會被重定向到 /oauth/error 即錯誤資訊端點中。whitelabel error (即Spring框架提供的一個預設錯誤頁面)錯誤端點提供了HTML的響應,但是我們大概可能需要實現一個自定義錯誤頁面(例如只是簡單的增加一個 @Controller 對映到請求路徑上 @RequestMapping("/oauth/error"))。

2.資源獲取服務

資源伺服器,其實就是存放一些受令牌保護的資源,只有令牌並且有效正確才能獲取到資源。 內部是通過Spring OAuth2的Spring Security Authentication filter 的過濾鏈來進行保護。

2.1.ResourceServerConfigurerAdapter

我們可以繼承ResourceServerConfigurerAdapter,來使用 ResourceServerSecurityConfigurer進行相關配置。

public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {

	@Override
	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().authenticated();
	}
}
複製程式碼

2.2.ResourceServerSecurityConfigurer的相關屬性

  • tokenServices:ResourceServerTokenServices 類的例項,用來實現令牌服務。
  • resourceId:這個資源服務的ID,這個屬性是可選的,但是推薦設定並在授權服務中進行驗證。
  • tokenExtractor 令牌提取器用來提取請求中的令牌。
  • 請求匹配器,用來設定需要進行保護的資源路徑,預設的情況下是受保護資源服務的全部路徑。
  • 受保護資源的訪問規則,預設的規則是簡單的身份驗證(plain authenticated)。
  • 其他的自定義許可權保護規則通過 HttpSecurity 來進行配置。

2.3.ResourceServerTokenServices

ResourceServerTokenServices 是組成授權服務的另一半。

1).若是資源伺服器和授權服務在同一個應用,可以使用DefaultTokenServices

2).若是分離的。ResourceServerTokenServices必須知道令牌的如何解碼。

ResourceServerTokenServices解析令牌的方法:

  • 使用RemoteTokenServices,資源伺服器通過HTTP請求來解碼令牌。每次都請求授權伺服器的端點-/oauth/check_toke,以此來解碼令牌
  • 若是訪問量大,則通過http獲取之後,換成令牌的結果
  • 若是jwt令牌,需請求授權服務的/oauth/token_key,來獲取key進行解碼

注:授權認證服務需要把/oauth/check_toke暴露出來,並且附帶上許可權訪問。

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
        .checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
複製程式碼

(~ ̄▽ ̄)~未完待續... ...

相關文章