OAuth 2.0 容許第三方應用程序訪問受限的HTTP資源的受權協議,像日常你們使用Github、Google帳號來登錄其餘系統時使用的就是 OAuth 2.0 受權框架,下圖就是使用Github帳號登錄Coding系統的受權頁面圖:html
相似使用 OAuth 2.0 受權的還有不少,本文將介紹 OAuth 2.0 相關的概念如:角色、受權類型等知識,如下是我整理一張 OAuth 2.0 受權的腦頭,但願對你們瞭解 OAuth 2.0 受權協議有幫助。git
文章將以腦圖中的內容展開 OAuth 2.0 協議同時除了 OAuth 2.0 外,還會配合 Spring Security OAuth2 來搭建OAuth2客戶端,這也是學習 OAuth 2.0 的目的,直接應用到實際項目中,加深對 OAuth 2.0 和 Spring Security 的理解。github
OAuth 2.0 中有四種類型的角色分別爲:資源Owner、受權服務、客戶端、資源服務,這四個角色負責不一樣的工做,爲了方便理解先給出一張大概的流程圖,細節部分後面再分別展開:web
OAuth 2.0 大概受權流程面試
資源 Owner能夠理解爲一個用戶,如以前提到使用Github登錄Coding中的例子中,用戶使用GitHub帳號登錄Coding,Coding就須要知道用戶在GitHub系統中的的頭像、用戶名、email等信息,這些帳戶信息都是屬於用戶的這樣就不難理解資源 Owner了。在Coding請求從GitHub中獲取想要的用戶信息時也是沒那容易的,GitHub爲了安全起見,至少要經過用戶(資源 Owner)的贊成才行。spring
明白資源 Owner後,相信你已經知道什麼是資源服務器,在這個例子中用戶帳號的信息都存放在GitHub的服務器中,因此這裏的資源服務器就是GitHub服務器。GitHub服務器負責保存、保護用戶的資源,任何其餘第三方系統想到使用這些信息的系統都須要通過資源 Owner受權,同時依照 OAuth 2.0 受權流程進行交互。數據庫
知道資源 Owner和資源服務器後,OAuth中的客戶端角色也相對容易理解了,簡單的說客戶端就是想要獲取資源的系統,如例子中的使用GitHub登錄Coding時,Coding就是OAuth中的客戶端。客戶端主要負責發起受權請求、獲取AccessToken、獲取用戶資源。api
有了資源 Owner、資源服務器、客戶端還不能完成OAuth受權的,還須要有受權服務器。在OAuth中受權服務器除了負責與用戶(資源 Owner)、客房端(Coding)交互外,還要生成AccessToken、驗證AccessToken等功能,它是OAuth受權中的很是重要的一環,在例子中受權服務器就是GitHub的服務器。瀏覽器
OAuth中:資源Owner、受權服務、客戶端、資源服務有四個角色在使用GitHub登錄Coding的例子中分別表示:緩存
OAuth2有三個重要的Endpoint其中受權 Endpoint、Token Endpoint結點在受權服務器中,還有一個可選的重定向 Endpoint在客戶端中。
經過四個OAuth角色,應該對OAuth協議有一個大概的認識,不過可能仍是一頭霧水不知道OAuth中的角色是如何交互的,不要緊繼續往下看一下受權類型就知道OAuth中的角色是如何完成本身的職責,進一步對OAuth的理解。在OAuth中定義了四種受權類型,分別爲:
這種形式就是咱們常見的受權形式(如使用GitHub帳號登錄Coding),在整個受權流程中會有資源Owner、受權服務器、客戶端三個OAuth角色參與,之因此叫作受權碼受權是由於在交互流程中受權服務器會給客房端發放一個code,隨後客房端拿着受權服務器發放的code繼續進行受權如:請求受權服務器發放AccessToken。
爲方便理解再將上圖的內容帶進真實的場景中,用文字表述一下整個流程:
D、Coding拿到code後,調用Github受權服務器API獲取AccessToken,因爲這一步是在Coding服務器後臺作的瀏覽器中捕獲不到,基本就是使用code訪問github的access_token節點獲取AccessToken;
以上是大體的受權碼受權流程,大部分是客戶端與受權服務器的交互,整個過程當中有幾個參數說明以下:
在使用受權碼受權的模式中,做爲客戶端請求受權的的時候都須要按規範請求,如下是使用受權碼受權發起受權時所須要的參數 :
在這裏插入圖片描述
如使用Github登錄Coding例子中的https://github.com/login/oauth/authorize?client_id=a5ce5a6c7e8c39567ca0&scope=user:email&redirect_uri=https://coding.net/api/oauth/github/callback&response_type=code受權請求URL,就有client_id、redirect_uri參數,至於爲啥沒有response_type在下猜測是由於Github給省了吧。
若是用戶贊成受權,那受權服務器也會返回標準的OAuth受權響應:
在這裏插入圖片描述
如Coding登錄中的https://coding.net/api/oauth/github/callback&response_type=code,用戶贊成受權後Github受權服務器回調Coding的回調地址,同時返回code、state參數。
客房端憑證受權受權的過程當中只會涉及客戶端與受權服務器交互,相比較其餘三種受權類型是比較簡單的。通常這種受權模式是用於服務之間受權,如在AWS中兩臺服務器分別爲應用服務器(A)和數據服務器(B),A 服務器須要訪問 B 服務器就須要經過受權服務器受權,而後才能去訪問 B 服務器獲取數據。
簡單二步就能夠完成客房端憑證受權啦,不過在使用客房端憑證受權時客戶端是直接訪問的受權服務器中獲取AccessToken接口。
客房端憑證受權中客戶端會直接發起獲取AccessToken請求受權服務器的AccessTokenEndpoint,請求參數以下:
在這裏插入圖片描述
注意: 在OAuth中AccessTokenEndpoint是使用HTTP Basic認證,在請求時還須要攜帶Authorization請求頭,如使用postman測試請求時:
其中的username和password參數對於OAuth協議中的client_id和client_secret,client_id和client_secret都是由受權服務器生成的。
客戶端憑證受權響應
受權服務器驗證完client_id和client_secret後返回token:
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "example_parameter":"example_value" }
用戶憑證受權與客戶端憑證受權相似,不一樣的地方是進行受權時要提供用戶名和用戶的密碼。
基本流程以下:
用戶憑證受權請求參數要比客戶端憑證受權多username和pwssword參數:
注意: 獲取Token時使用HTTP Basic認證,與客戶端憑證受權同樣。
用戶憑證受權響應與客戶端憑證受權差很少:
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
隱式受權用於獲取AccessToken,可是獲取的方式與用戶憑證受權和客戶端受權不一樣的是,它是在訪問受權Endpoint的時候就會獲取AccessToken而不是訪問Token Endpoing,並且AccessToken的會做爲redirect_uri的Segment返回。
再使用隱式受權時,所須要請求參數以下:
在這裏插入圖片描述
隱式受權響應參數是經過redirect_uri回調返回的,如http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600就是隱式受權響應參數,其中須要注意的是響應的參數是使用Segment的形式的,而不是普通的URL參數。
在這裏插入圖片描述
前面提到過OAuth協議中有四個角色,這一節使用Spring Boot實現一個登錄GitHub的OAuthClient,要使用OAuth2協議登錄GitHub首先要雲GitHub裏面申請:
申請 OAuth App
OAuth Apps
填寫必需的信息
在這裏插入圖片描述
上圖中的Authorization callback URL就是redirect_uri用戶贊成受權後GitHub會將瀏覽器重定向到該地址,所以先要在本地的OAuth客戶端服務中添加一個接口響應GitHub的重定向請求。
熟悉OAuth2協議後,咱們在使用 Spring Security OAuth2 配置一個GitHub受權客戶端,使用認證碼受權流程(能夠先去看一遍認證碼受權流程圖),示例工程依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Spring Security OAuth2 默認集成了Github、Goolge等經常使用的受權服務器,由於這些經常使用的受權服務的配置信息都是公開的,Spring Security OAuth2 已經幫咱們配置了,開發都只須要指定必需的信息就行如:clientId、clientSecret。
Spring Security OAuth2使用Registration做爲客戶端的的配置實體:
public static class Registration { //受權服務器提供者名稱 private String provider; //客戶端id private String clientId; //客戶端憑證 private String clientSecret; ....
下面是以前註冊好的 GitHub OAuth App 的信息:
spring.security.oauth2.client.registration.github.clientId=5fefca2daccf85bede32 spring.security.oauth2.client.registration.github.clientSecret=01dde7a7239bd18bd8a83de67f99dde864fb6524``
Spring Security OAuth2內置了一個redirect_uri模板:{baseUrl}/login/oauth2/code/{registrationId},其中的registrationId
是在從配置中提取出來的:
spring.security.oauth2.client.registration.[registrationId].clientId=xxxxx
如在上面的GitHub客戶端的配置中,由於指定的registrationId是github,因此重定向uri地址就是:
{baseUrl}/login/oauth2/code/github
OAuth2客戶端和重定向Uri配置好後,將服務器啓動,而後打開瀏覽器進入:http://localhost:8080/。第一次打開由於沒有認證會將瀏覽器重客向到GitHub的受權Endpoint:
在這裏插入圖片描述
Spring Security OAuth2內置了一些經常使用的受權服務器的配置,這些配置都在CommonOAuth2Provider中:
public enum CommonOAuth2Provider { GOOGLE { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email"); builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); builder.tokenUri("https://www.googleapis.com/oauth2/v4/token"); builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs"); builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Google"); return builder; } }, GITHUB { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("read:user"); builder.authorizationUri("https://github.com/login/oauth/authorize"); builder.tokenUri("https://github.com/login/oauth/access_token"); builder.userInfoUri("https://api.github.com/user"); builder.userNameAttributeName("id"); builder.clientName("GitHub"); return builder; } }, FACEBOOK { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL); builder.scope("public_profile", "email"); builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth"); builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token"); builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email"); builder.userNameAttributeName("id"); builder.clientName("Facebook"); return builder; } }, OKTA { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Okta"); return builder; } }; private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; }
CommonOAuth2Provider中有四個受權服務器配置:OKTA、FACEBOOK 、GITHUB 、GOOGLE。在OAuth2協議中的配置項redirect_uri、Token Endpoint、受權 Endpoint、scope都會在這裏配置:
GITHUB { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("read:user"); builder.authorizationUri("https://github.com/login/oauth/authorize"); builder.tokenUri("https://github.com/login/oauth/access_token"); builder.userInfoUri("https://api.github.com/user"); builder.userNameAttributeName("id"); builder.clientName("GitHub"); return builder; } }
腦瓜子有點蒙了,感受本身就配置了clientid和clientSecret一個OAuth2客戶端就完成了,其中的一些起因還沒搞明白啊。。。,最好奇的是重定向Uri是怎麼被處理的。
Spring Security OAuth2 是基於 Spring Security 的,以前看過Spring Security文章,知道它的處理原理是基於過濾器的,若是你不知道的話推薦看這篇文章:《Spring Security 架構》。在源碼中找了一下,發現一個可疑的Security 過濾器:
public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";
是一個匹配器,以前提到過Spring Security OAuth2中有一個默認的redirect_uri模板:{baseUrl}/{action}/oauth2/code/{registrationId},/login/oauth2/code/*正好能與redirect_uri模板匹配成功,因此OAuth2LoginAuthenticationFilter會在用戶贊成受權後執行,它的構造方法以下:
public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { this(clientRegistrationRepository, authorizedClientService, DEFAULT_FILTER_PROCESSES_URI); }
OAuth2LoginAuthenticationFilter 主要將受權服務器返回的code拿出來,而後經過AuthenticationManager 來認證(獲取AccessToken),下來是移除部分代碼後的源代碼:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); //檢查沒code與state if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } //獲取 OAuth2AuthorizationRequest OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } //取出 ClientRegistration String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE, "Client Registration not found with Id: " + registrationId, null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replaceQuery(null) .build() .toUriString(); //認證、獲取AccessToken OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri); Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request); OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken( clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); authenticationRequest.setDetails(authenticationDetails); OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest); ... return oauth2Authentication; }
前面提到OAuth2LoginAuthenticationFilter是使用 AuthenticationManager 來進行OAuth2認證的,通常狀況下在 Spring Security 中的 AuthenticationManager 都是使用的 ProviderManager 來進行認證的,因此對應在 Spring Security OAuth2 中有一個 OAuth2LoginAuthenticationProvider 用於獲取AccessToken:
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient; private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService; private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); .... @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // scope // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. if (authorizationCodeAuthentication.getAuthorizationExchange() .getAuthorizationRequest().getScopes().contains("openid")) { // This is an OpenID Connect Authentication Request so return null // and let OidcAuthorizationCodeAuthenticationProvider handle it instead return null; } OAuth2AccessTokenResponse accessTokenResponse; try { OAuth2AuthorizationExchangeValidator.validate( authorizationCodeAuthentication.getAuthorizationExchange()); //訪問GitHub TokenEndpoint獲取Token accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( new OAuth2AuthorizationCodeGrantRequest( authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); } catch (OAuth2AuthorizationException ex) { OAuth2Error oauth2Error = ex.getError(); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } ... return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } }
-END-
架構文摘
ArchDigest
架構知識丨大型網站丨大數據丨機器學習若有收穫,點個在看,誠摯感謝圖片