1. 程式人生 > >基於OAuth的統一認證原理解析和例項分析

基於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 serviceHTTP服務提供商,本文中簡稱”服務提供商”,即上一節例子中的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開源的程式碼