基於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開源的代碼

最後感謝 大神阮一峯的博客Oauth2理解
以及慕課網博客 http://www.imooc.com/article/10931

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章