基於OAuth的統一認證原理解析和例項分析
理解OAuth 2.0
在我們去了解OAuth的原理和分析其例項之前我們先來理解一下Oauth的概念和基本知識:
OAuth是一個關於授權(authorization)的開放網路標準,在全世界得到廣泛應用,目前的版本是2.0版。
本文對OAuth 2.0的設計思路和執行流程,做一個簡明通俗的解釋,主要參考材料為RFC 6749。
一、應用場景
為了理解OAuth的適用場合,讓我舉一個假設的例子。
有一個”雲沖印”的網站,可以將使用者儲存在Google的照片,沖印出來。使用者為了使用該服務,必須讓”雲沖印”讀取自己儲存在Google上的照片。
問題是隻有得到使用者的授權,Google才會同意”雲沖印”讀取這些照片。那麼,”雲沖印”怎樣獲得使用者的授權呢?
傳統方法是,使用者將自己的Google使用者名稱和密碼,告訴”雲沖印”,後者就可以讀取使用者的照片了。這樣的做法有以下幾個嚴重的缺點。
問題是隻有得到使用者的授權,Google才會同意”雲沖印”讀取這些照片。那麼,”雲沖印”怎樣獲得使用者的授權呢?
傳統方法是,使用者將自己的Google使用者名稱和密碼,告訴”雲沖印”,後者就可以讀取使用者的照片了。這樣的做法有以下幾個嚴重的缺點。
(1)"雲沖印"為了後續的服務,會儲存使用者的密碼,這樣很不安全。 (2)Google不得不部署密碼登入,而我們知道,單純的密碼登入並不安全。 (3)"雲沖印"擁有了獲取使用者儲存在Google所有資料的權力,使用者沒法限制"雲沖印"獲得授權的範圍和有效期。 (4)使用者只有修改密碼,才能收回賦予"雲沖印"的權力。但是這樣做,會使得其他所有獲得使用者授權的第三方應用程式全部失效。 (5)只要有一個第三方應用程式被破解,就會導致使用者密碼洩漏,以及所有被密碼保護的資料洩漏。
OAuth就是為了解決上面這些問題而誕生的。
二、名詞定義(角色定義)
在詳細講解OAuth 2.0之前,需要了解幾個專用名詞。它們對讀懂後面的講解,尤其是幾張圖,至關重要。
(1) Third-party application:第三方應用程式,本文中又稱”客戶端”(client),即上一節例子中的”雲沖印”。
(2)HTTP service:HTTP服務提供商,本文中簡稱”服務提供商”,即上一節例子中的Google。
(3)Resource Owner:資源所有者,本文中又稱”使用者”(user)。
(4)User Agent:使用者代理,本文中就是指瀏覽器。
(5)Authorization server
(6)Resource server:資源伺服器,即服務提供商存放使用者生成的資源的伺服器。它與認證伺服器,可以是同一臺伺服器,也可以是不同的伺服器。
知道了上面這些名詞,就不難理解,OAuth的作用就是讓”客戶端”安全可控地獲取”使用者”的授權,與”服務商提供商”進行互動。
三、OAuth的思路
OAuth在”客戶端”與”服務提供商”之間,設定了一個授權層(authorization layer)。”客戶端”不能直接登入”服務提供商”,只能登入授權層,以此將使用者與客戶端區分開來。”客戶端”登入授權層所用的令牌(token),與使用者的密碼不同。使用者可以在登入的時候,指定授權層令牌的許可權範圍和有效期。
“客戶端”登入授權層以後,”服務提供商”根據令牌的許可權範圍和有效期,向”客戶端”開放使用者儲存的資料。
四、執行流程
OAuth 2.0的執行流程如下圖,摘自RFC 6749。
OAuth執行流程
(A)使用者開啟客戶端以後,客戶端要求使用者給予授權。
(B)使用者同意給予客戶端授權。
(C)客戶端使用上一步獲得的授權,向認證伺服器申請令牌。
(D)認證伺服器對客戶端進行認證以後,確認無誤,同意發放令牌。
(E)客戶端使用令牌,向資源伺服器申請獲取資源。
(F)資源伺服器確認令牌無誤,同意向客戶端開放資源。
不難看出來,上面六個步驟之中,B是關鍵,即使用者怎樣才能給於客戶端授權。有了這個授權以後,客戶端就可以獲取令牌,進而憑令牌獲取資源。
下面一一講解客戶端獲取授權的四種模式。
五、客戶端的授權模式
客戶端必須得到使用者的授權(authorization grant),才能獲得令牌(access token)。OAuth 2.0定義了四種授權方式。
授權碼模式(authorization code)
簡化模式(implicit)
密碼模式(resource owner password credentials)
六、授權碼模式
授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與”服務提供商”的認證伺服器進行互動,這裡呢我們也重點來講講這個授權碼模式。
它的步驟如下:
(A)使用者訪問客戶端,後者將前者導向認證伺服器。
(B)使用者選擇是否給予客戶端授權。
(C)假設使用者給予授權,認證伺服器將使用者導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
(D)客戶端收到授權碼,附上早先的"重定向URI",向認證伺服器申請令牌。這一步是在客戶端的後臺的伺服器上完成的,對使用者不可見。
(E)認證伺服器核對了授權碼和重定向URI,確認無誤後,向客戶端傳送訪問令牌(access token)和更新令牌(refresh token)。
下面是上面這些步驟所需要的引數。
A步驟中,客戶端申請認證的URI,包含以下引數:
response_type:表示授權型別,必選項,此處的值固定為"code"
client_id:表示客戶端的ID,必選項
redirect_uri:表示重定向URI,可選項
scope:表示申請的許可權範圍,可選項
state:表示客戶端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。
下面是一個例子。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
C步驟中,伺服器迴應客戶端的URI,包含以下引數:
code:表示授權碼,必選項。該碼的有效期應該很短,通常設為10分鐘,客戶端只能使用該碼一次,否則會被授權伺服器拒絕。該碼與客戶端ID和重定向URI,是一一對應關係。
state:如果客戶端的請求中包含這個引數,認證伺服器的迴應也必須一模一樣包含這個引數。
下面是一個例子。
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
D步驟中,客戶端向認證伺服器申請令牌的HTTP請求,包含以下引數:
grant_type:表示使用的授權模式,必選項,此處的值固定為"authorization_code"。
code:表示上一步獲得的授權碼,必選項。
redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該引數值保持一致。
client_id:表示客戶端ID,必選項。
下面是一個例子。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
E步驟中,認證伺服器傳送的HTTP回覆,包含以下引數:
access_token:表示訪問令牌,必選項。
token_type:表示令牌型別,該值大小寫不敏感,必選項,可以是bearer型別或mac型別。
expires_in:表示過期時間,單位為秒。如果省略該引數,必須其他方式設定過期時間。
refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
scope:表示許可權範圍,如果與客戶端申請的範圍一致,此項可省略。
下面是一個例子。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
從上面程式碼可以看到,相關引數使用JSON格式傳送(Content-Type: application/json)。此外,HTTP頭資訊中明確指定不得快取。
以上講解的呢就是我們的Oauth的授權碼驗證模式,接下來我們來解析一個例項來使用授權碼驗證模式。
方式認證例項
假設現在有一個應用伺服器跑在我本機8000埠,認證伺服器在8090埠。在需要使用者登入時候把使用者帶到以下的一個URL.
http://localhost:8090/oauth/authorize?response_type=code&scope=read write&client_id=test&redirect_uri=http://localhost:8000/login&state=09876999
我們注意到幾個重要的引數:
在這幾個引數中分別包含了客戶端Id(client_id) 、重定向uri(redirect_Uri)、授權型別(response_type)、申請的許可權範圍(scope)、客戶端當前狀態(state)可以是任意值。
response_type:表示授權型別,就是上面講的那四種類型,這裡用的是code方式。
client_id:表示客戶端的ID,代表哪個應用請求驗證
redirect_uri:表示重定向URI,驗證以後的回撥地址,一般用來接收返回的code,以及做下一步處理。
scope:表示申請的許可權範圍,
state:表示客戶端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。作為安全校驗。
下面是驗證伺服器接受這個請求的控制器關鍵程式碼:
@RequestMapping("authorize")
public void authorize(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
OAuthAuthxRequest oauthRequest = new OAuthAuthxRequest(request);
if (oauthRequest.isCode()) {
CodeAuthorizeHandler codeAuthorizeHandler = new CodeAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'code' handler: {}", codeAuthorizeHandler);
codeAuthorizeHandler.handle();
} else if (oauthRequest.isToken()) {
TokenAuthorizeHandler tokenAuthorizeHandler = new TokenAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'token' handler: {}", tokenAuthorizeHandler);
tokenAuthorizeHandler.handle();
} else {
unsupportResponseType(oauthRequest, response);
}
}
}
首先拿到這個請求以後根據請求的引數將其封裝成一個OAuthAuthxRequest,基本就是把請求過來的引數,方法繫結便於使用。這是由oltu提供的OAuthRequest的進一步封裝。
然後判斷這個請求的授權的型別是否是code,也就是判斷下請求引數的response_type是否為code,可以看到目前製作了兩種型別的授權。
然後根據對應的授權型別,構造對應的方法處理器。下面是handle的實現介面:
public void handle() throws OAuthSystemException, ServletException, IOException {
//驗證請求是否合法,主要是針對引數做基本的校驗,重定向連結,客戶端ID授權範圍等這些資訊與註冊的是否相同。
if (validateFailed()) {
return;
}
//判斷使用者是否登入過,根據session判斷。因此多個應用使用同一個授權服務的話,是可以直接跳過登入步驟的也就實現了單點登入的效果。如果沒有登入的話,這一步的請求會被重定向至登入頁面。(登入也得隱藏域會帶上這些引數)
if (goLogin()) {
return;
}
//這個請求如果是從登入頁面提交過來的,那麼就提交使用者的登入,這個框架中交給shiro去做登入相關的操作。
if (submitLogin()) {
return;
}
// 本系統中把登入和授權放在兩個步驟中完成,有點像新浪微博的方式,QQ是一步完成授權。使用者未授權則跳轉授權頁面
if (goApproval()) {
return;
}
//與登入類似,也是提交使用者批准或拒絕了許可權請求
if (submitApproval()) {
return;
}
//以上任意一步沒有通過都是授權失敗會進行相應處理,如果都通過了就發放Code碼。
handleResponse();
}
如果以上步驟都通過的話,認證伺服器會轉向這個會調地址,帶上發放的Code碼,類似如下:
http://localhost:8000/login?code=bca654ab6133ab3cbc55bb751da93b1c&state=09876999
可以看到帶回了返回的引數,以及原樣返回的狀態碼。
應用伺服器這時候拿到返回的code去換token,發起如下的一個請求:
localhost:8090/oauth/token?client_id=test&client_secret=test&grant_type=authorization_code&code=bca654ab6133ab3cbc55bb751da93b1c&redirect_uri=http://localhost:8000/login&scope=read%20write&state=09876999
與之前請求類似只是多了一個code欄位,去驗證客戶端的合法性。
驗證伺服器會在收到code以後去查詢是否有支援這種code的處理器,如果有則發放token。
for (OAuthTokenHandler handler : handlers) {
if (handler.support(tokenRequest)) {
LOG.debug("Found '{}' handle OAuthTokenxRequest: {}", handler, tokenRequest);
handler.handle(tokenRequest, response);
return;
}
}
初始化支援的handler
private void initialHandlers() {
handlers.add(new AuthorizationCodeTokenHandler());
handlers.add(new PasswordTokenHandler());
handlers.add(new RefreshTokenHandler());
handlers.add(new ClientCredentialsTokenHandler());
}
驗證通過後應用伺服器會接受到包含token的一個json資料:
{
"access_token": "23e003b5e4b9b7eda228b845532d8336",
"refresh_token": "d6b49710f398c405a62f31a6676c5830",
"token_type": "Bearer",
"expires_in": 43199
}
這個token是有一定的有效期的,在服務端會快取這個token以便下一次查詢,應用客戶端也應該保留這個token,訪問受限資源時候需要帶上這個token去驗證身份。
比如請求一個API如下:
curl -i -X GET \
-H "Authorization:Bearer 33dbfc80f5659c6fdec73a044ff724c3" \
'http://localhost:8090/api/test'
資源伺服器上使用shiro做安全驗證,配置OAuth2對應的realms即可:
<property name="realms">
<list>
<bean id="systemAuthorizingRealm" class="me.kbiao.example.modules.sys.security.SystemAuthorizingRealm"/>
<bean id="oAuth2Realm" class="me.kbiao.example..modules.sys.security.OAuth2Realm"/>
</list>
</property>
在這個reamls中根據token去查到使用者資訊,再去分發對應的資源。
自此便完成了整個oauth2的流程。
這個流程中認證服務系統需要配置三張資料表:
client_details表中存放註冊的客戶端資料。如回撥地址,授權型別,是否信任,許可權資訊等
code中存放發放給客戶端應用的code,使用後失效,以保證安全性
access_token中存放使用者資訊、客戶端和token的對應關係。
專案是基於Shiro+ALTU實現,參考方案mkk/oauth2-shiro - 碼雲 - 開源中國 ,更詳細的內容,可以去讀讀Shengzhao Li開源的程式碼