OAuth 2.0 允許第三方應用程序訪問受限的HTTP資源的授權協議,像平常大家使用Github
、Google
賬號來登陸其他系統時使用的就是 OAuth 2.0 授權框架,下圖就是使用Github
賬號登陸Coding
系統的授權頁面圖:
類似使用 OAuth 2.0 授權的還有很多,本文將介紹 OAuth 2.0 相關的概念如:角色、授權類型等知識,以下是我整理一張 OAuth 2.0 授權的腦頭,希望對大家瞭解 OAuth 2.0 授權協議有幫助。
文章將以腦圖中的內容展開 OAuth 2.0 協議同時除了 OAuth 2.0 外,還會配合 Spring Security OAuth2 來搭建OAuth2客戶端
,這也是學習 OAuth 2.0 的目的,直接應用到實際項目中,加深對 OAuth 2.0 和 Spring Security 的理解。
OAuth 2.0 角色
OAuth 2.0 中有四種類型的角色分別爲:資源Owner
、授權服務
、客戶端
、資源服務
,這四個角色負責不同的工作,爲了方便理解先給出一張大概的流程圖,細節部分後面再分別展開:
OAuth 2.0 大概授權流程
資源 Owner
資源 Owner可以理解爲一個用戶,如之前提到使用Github
登陸Coding
中的例子中,用戶使用GitHub賬號登陸Coding,Coding就需要知道用戶在GitHub系統中的的頭像、用戶名、email等信息,這些賬戶信息都是屬於用戶的這樣就不難理解資源 Owner
了。在Coding請求從GitHub中獲取想要的用戶信息時也是沒那容易的,GitHub爲了安全起見,至少要通過用戶(資源 Owner)的同意才行。
資源服務器
明白資源 Owner
後,相信你已經知道什麼是資源服務器
,在這個例子中用戶賬號的信息都存放在GitHub的服務器中,所以這裏的資源服務器就是GitHub服務器。GitHub服務器負責保存、保護用戶的資源,任何其他第三方系統想到使用這些信息的系統都需要經過資源 Owner
授權,同時依照 OAuth 2.0 授權流程進行交互。
客戶端
知道資源 Owner
和資源服務器
後,OAuth中的客戶端角色也相對容易理解了,簡單的說客戶端就是想要獲取資源的系統,如例子中的使用GitHub登陸Coding時,Coding就是OAuth中的客戶端。客戶端主要負責發起授權請求、獲取AccessToken、獲取用戶資源。
授權服務器
有了資源 Owner
、資源服務器
、客戶端
還不能完成OAuth授權的,還需要有授權服務器
。在OAuth中授權服務器除了負責與用戶(資源 Owner)、客房端(Coding)交互外,還要生成AccessToken、驗證AccessToken等功能,它是OAuth授權中的非常重要的一環,在例子中授權服務器就是GitHub的服務器。
小結
OAuth中:資源Owner
、授權服務
、客戶端
、資源服務
有四個角色在使用GitHub登陸Coding的例子中分別表示:
-
資源Owner:GitHub用戶
-
授權服務:GitHub服務器
-
客戶端:Coding系統
-
資源服務:GitHub服務器
其中授權服務服務器、資源服務器可以單獨搭建(鬼知道GitHub怎麼搭建的)。在微服務器架構中可單獨弄一個授權服務,資源服務服務可以多個如:用戶資源、倉庫資源等,可根據需求自由分服務。
OAuth2 Endpoint
OAuth2有三個重要的Endpoint其中授權 Endpoint
、Token Endpoint
結點在授權服務器中,還有一個可選的重定向 Endpoint
在客戶端中。
-
授權 Endpoint:使用
授權 Endpoint
去獲取資源Owner的授權 -
Token Endpoint:客戶端獲取token
-
重定向 Endpoint:授權服務器使用
重定向 Endpoint
返回授權響應給客戶端
授權類型
通過四個OAuth角色,應該對OAuth協議有一個大概的認識,不過可能還是一頭霧水不知道OAuth中的角色是如何交互的,沒關係繼續往下看一下授權類型
就知道OAuth中的角色是如何完成自己的職責,進一步對OAuth的理解。在OAuth中定義了四種授權類型
,分別爲:
-
授權碼授權
-
客房端憑證授權
-
資源Owner的密碼授權
-
隱式的授權
不同的授權類型
可以使用在不同的場景中。
授權碼授權
這種形式就是我們常見的授權形式(如使用GitHub賬號登陸Coding),在整個授權流程中會有資源Owner
、授權服務器
、客戶端
三個OAuth角色參與,之所以叫做授權碼授權
是因爲在交互流程中授權服務器會給客房端發放一個code
,隨後客房端拿着授權服務器發放的code繼續進行授權如:請求授權服務器發放AccessToken。
爲方便理解再將上圖的內容帶進真實的場景中,用文字表述一下整個流程:
-
A.1、用戶訪問Coding登陸頁(
https://coding.net/login
),點擊Github登陸按鈕; -
A.2、Coding服務器將瀏覽器重定向到Github的授權頁(
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
參數; -
B.1、用戶輸入用戶名、密碼登陸Github;
-
B.2、用戶點擊
授權按鈕
,同意授權; -
C.1、Github授權服務器返回
code
; -
C.2、Github通過將瀏覽器重定向到
A.2
步驟中傳遞的redirect_uri
地址(https://coding.net/api/oauth/github/callback&response_type=code); -
D、Coding拿到code後,調用Github授權服務器API獲取AccessToken,由於這一步是在Coding服務器後臺做的瀏覽器中捕獲不到,基本就是使用
code
訪問github的access_token節點獲取AccessToken;
以上是大致的授權碼授權
流程,大部分是客戶端與授權服務器的交互,整個過程中有幾個參數說明如下:
-
client_id:在Github中註冊的Appid,用於標記客戶端
-
redirect_uri:可以理解一個
callback
,授權服務器驗證完客戶端與用戶名等信息後將瀏覽器重定向到此地址並帶上code
參數 -
code:由授權服務器返回的一個憑證,用於獲取AccessToken
-
state:由客戶端傳遞給授權服務器,授權服務器一般到調用
redirect_uri
時原樣返回
授權碼授權請求
在使用授權碼授權的模式中,作爲客戶端請求授權的的時候都需要按規範請求,以下是使用授權碼授權發起授權時所需要的參數 :
在這裏插入圖片描述
如使用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請求授權服務器的AccessToken
Endpoint,請求參數如下:
在這裏插入圖片描述
注意: 在OAuth中AccessToken
Endpoint是使用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"
}
用戶憑證授權
用戶憑證授權
與客戶端憑證授權
類似,不同的地方是進行授權時要提供用戶名和用戶的密碼。
基本流程如下:
-
A、客戶端首先需要知道用戶的憑證
-
B、使用用戶憑證獲取AccessToken
-
C、授權服務器驗證客戶端與用戶憑證,返回AccessToken
用戶憑證授權請求
用戶憑證授權請求參數要比客戶端憑證授權多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返回。
-
A.1、A.2、瀏覽器訪問支持隱式授權的服務器的授權Endpoint;
-
B.1、用戶輸入賬號密碼;
-
B.2、用戶點擊
授權按鈕
,同意授權; -
C、授權服務器使用
redirect_uri
返回AccessToken; -
D、授權服務器將瀏覽器重定向到
redirect_uri
,並攜帶AccessToken如:http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600
; -
D、
redirect_uri
的地址是指向一個Web資源客戶端
-
E、
Web資源客戶端
返回一段腳本 -
F、瀏覽器執行腳本
-
D、客戶端獲得AccessToken
隱式授權不太好理解,但是仔細比較客戶端憑證授權
和用戶憑證授權
會發現隱式授權
不需要知道用戶憑證
或客戶端憑證
,這樣做相對更安全。
隱式授權請求
再使用隱式授權
時,所需要請求參數如下:
在這裏插入圖片描述
隱式授權響應
隱式授權響應參數是通過redirect_uri
回調返回的,如http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600
就是隱式授權響應參數,其中需要注意的是響應的參數是使用Segment的形式的,而不是普通的URL參數。
在這裏插入圖片描述
OAuth2 客戶端
前面提到過OAuth協議中有四個角色,這一節使用Spring Boot實現一個登陸GitHub
的OAuthClient
,要使用OAuth2協議登陸GitHub首先要雲GitHub裏面申請:
申請 OAuth App
OAuth Apps
填寫必需的信息
上圖中的Authorization callback URL
就是redirect_uri
用戶同意授權後GitHub會將瀏覽器重定向到該地址,因此先要在本地的OAuth客戶端服務中添加一個接口響應GitHub的重定向請求。
配置OAuthClient
熟悉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``
配置redirect_uri
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
:
在這裏插入圖片描述
常用授權服務器(CommonOAuth2Provider)
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;
}
}
重定向Uri攔截
腦瓜子有點蒙了,感覺自己就配置了clientid
和clientSecret
一個OAuth2客戶端就完成了,其中的一些原由還沒搞明白啊。。。,最好奇的是重定向Uri是怎麼被處理的。
Spring Security OAuth2
是基於 Spring Security
的,之前看過Spring Security
文章,知道它的處理原理是基於過濾器的,如果你不知道的話推薦看這篇文章:《Spring Security 架構》。在源碼中找了一下,發現一個可疑的Security 過濾器:
-
OAuth2LoginAuthenticationFilter:處理OAuth2授權的過濾器
這個 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;
}
獲取AccessToken
前面提到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);
}