1. 程式人生 > >建立自己的OAuth2.0服務端(一)

建立自己的OAuth2.0服務端(一)

原文: 建立自己的OAuth2.0服務端(一)

 

如果對OAuth2.0有任何的疑問,請先熟悉OAuth2.0基礎的文章:http://www.cnblogs.com/alunchen/p/6956016.html

1. 前言

本篇文章時對 客戶端的授權模式-授權碼模式 的建立,當然你理解的最複雜的模式之後,其他模式都是在授權碼模式上面做一些小改動即可。對於授權碼模式有任何的疑問,請看上面提到的文章。

注意:本文是建立OAuth的Server端,不是Client請求端。

 

2. 開始授權碼模式的概念、流程

第2點其實就是複製了上一篇文章,為了提高閱讀性,讀過上一篇文章的可略過第2點。

授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺伺服器,與"服務提供商"的認證伺服器進行互動。

流程圖:

image

圖說明:

  (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頭資訊中明確指定不得快取。

 

3. 開始寫自己的OAuth2.0服務端程式碼(C#)

如果有錯誤的地方,歡迎指正,作者也不保證完全正確。

這裡介紹的是C#,當然你可以用你自己的語言寫,大同小異。(沒用到第三方關於OAuth2.0的框架)

作者在開始理解OAuth2.0的概念時,化了一段比較長的時間。從微信授權開始接觸OAuth2.0的概念,後來寫了一套第三方微信授權的小程式,慢慢消化OAuth2.0。

說實在的,OAuth2.0安全在於,提供了code、access_token,來繫結我們的使用者資訊。並且code、access_token有過期的時間。所以,關鍵在於理解code與access_token的作用。

開始程式碼,我們建立一個MVC的程式,這裡叫做MyOAuth2Server。

 

3.1開始授權驗證

第一步,開始授權驗證,並且跳轉到指定的授權頁面。

先上程式碼,然後再分析:

        /// <summary>
        /// 第一步,開始授權驗證
         /// 指定到使用者授權的頁面
         /// </summary>
        /// <param name="client_id"></param>
        /// <param name="response_type"></param>
        /// <param name="redirect_uri"></param>
        /// <param name="state"></param>
        /// <param name="scope"></param>
        /// <returns></returns>
        public ActionResult Authorize(string client_id, string response_type, string redirect_uri, string scope, string state = "")
        {
            if ("code".Equals(response_type))
            {
                //判斷client_id是否可用
                if (!_oAuth2ServerServices.IsClientIdValied(client_id))
                    return this.Json("client_id 無效", JsonRequestBehavior.AllowGet);

                //儲存使用者請求的所有資訊到指定容器(session)
                   Session.Add("client_id", client_id);
                Session.Add("response_type", response_type);
                Session.Add("redirect_uri", redirect_uri);
                Session.Add("state", state);
                Session.Add("scope", scope);
                //重定向到使用者授權頁面(當然你可以自定義自己的頁面)
                return View("Authorize");
            }
            return View("Error");
        }

客戶端會授權會請求我們授權驗證方法,

     首先,驗證client_id是否可用,這裡的client_id是為了保證安全性,確保請求端是服務端給予請求或者授權的權利。簡單地說,就是請求端在用此服務端之前要申請唯一的一個client_id;

     然後,在把客戶端傳過來的資訊儲存在Session(你也可以儲存在其他地方);

     最後,跳轉到使用者操作的,是否給予授權的頁面(可以是點選一個確定授權的按鈕,類似於微信授權。也可以是輸入使用者名稱&密碼等的頁面)。

下面我們看一下 return View("Authorize"); 這句程式碼所返回給使用者許可的頁面:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>授權驗證</title>
</head>
<body>
    <div>
        你確定給使用者授權嗎?
        <form action="/OAuth2Server/Authenticate" method="post" id="login_form">
            <br /><br />
            <table class="form_field">
                <tr>
                    <td class="right">
                        User:
                    </td>
                    <td>
                        <input type="text" name="user" id="user" style="width: 12em;">
                    </td>
                </tr>
                <tr>
                    <td class="right">
                        Password:
                    </td>
                    <td>
                        <input type="password" name="password" id="password" style="width: 12em;">
                    </td>
                </tr>
                <tr></tr>
            </table>
            <div class="action">
                <input type="submit" value="授權" />
            </div>
            <br />
        </form>
    </div>
</body>
</html>

從上面可以看到,使用者確定授權後會提交資訊到Authenticate方法,下面我們看看Authenticate到底是做了什麼。

 

3.2驗證並返回code到請求端

我們這裡是使用者名稱與密碼驗證,當然你也可以用其他驗證。(比如使用者點選一個授權允許的按鈕就可以了)

     首先,在OAuth2.0服務端上驗證使用者輸入的使用者名稱與密碼是否正確;

     然後,生成code,並且設定code的生存時間,預設是30秒。(code只能用一次,之後要刪除);

     再繫結code與使用者資訊(使用者唯一鍵);

     最後,重定向回redirect_uri請求的地址,並且返回code與state。(state是請求端那邊想要用於處理一些業務邏輯所用到的,當然可以為空)

上程式碼

        /// <summary>
        /// 第二步,使用者確認授權後的操作。
         /// 使用者確認授權後,則返回code、access_token,並重定向到redirect_uri所指定的頁面
         /// </summary>
        /// <returns></returns>
        public ActionResult Authenticate()
        {
            var username = Request["user"] ?? "";
            var password = Request["password"] ?? "";

            //取得重定向的資訊
            var redirect_uri = Session["redirect_uri"] ?? "";
            var state = Session["state"] ?? "";
            string code = TokenCodeUtil.GetCode();

            //驗證使用者名稱密碼
            if (!_oAuth2ServerServices.IsUserValied(username, password))
                return this.Json("使用者名稱或密碼不正確", JsonRequestBehavior.AllowGet);

            //儲存code到DB/Redis,預設存在30秒
            _oAuth2ServerServices.SaveCode(code);
            //繫結code與userid,因為後面查詢使用者資訊的時候要用到,預設存在30秒
            _oAuth2ServerServices.BingCodeAndUser(username, code);
            //重定向
            string url = string.Format(HttpUtility.UrlDecode(redirect_uri.ToString()) + "?code={0}&state={1}", code, state);
            Response.Redirect(url);

            return null;
        }

上面,已經完成了code的使命,並且返回到了請求端。

下面,我們來看看怎麼獲取token。

 

3.3獲取token

在請求端獲取到code之後,請求端要獲取token,因為獲取了token請求端才能獲取到使用者資訊等資料。

     首先,把token設定成不能保持cache的狀態,為了保證安全性;

     然後,判斷是獲取token還是重新整理token的狀態;

     再驗證code是否過期,驗證client_id、client_secret是否正確;

     再生成token,把token存入容器(DB、Redis、Memory等)中;

     在通過code來獲取使用者的資訊,把使用者資訊(主鍵)與token做繫結;

     最後,把code刪除(code只能用一次,如果想再獲取token只能第一步開始重新做),返回token。

上程式碼:

        /// <summary>
        /// 獲取或重新整理token。
         /// token可能儲存在DB/Redis等
         /// </summary>
        /// <param name="code"></param>
        /// <param name="grant_type"></param>
        /// <param name="client_id"></param>
        /// <param name="client_secret"></param>
        /// <returns></returns>
        public ActionResult GetToken(string code, string grant_type, string client_id, string client_secret)
        {
            Response.ContentType = "application/json";
            Response.AddHeader("Cache-Control", "no-store");

            //獲取token
            if (grant_type == "authorization_code")
            {
                //判斷code是否過期
                if (!_oAuth2ServerServices.IsCodeValied(code, DateTime.Now))
                    return this.Json("code 過期", JsonRequestBehavior.AllowGet);
                //判斷client_id與client_secret是否正確
                if (!_oAuth2ServerServices.IsClientValied(client_id, client_secret))
                    return this.Json("client_id、client_secret不正確", JsonRequestBehavior.AllowGet);
                //新建token
                string access_token = TokenCodeUtil.GetToken();
                //儲存token,預設是30分鐘
                _oAuth2ServerServices.SaveToken(access_token);
                //通過code獲取userid,然後用token與userid做繫結,最後把code設定成消失(刪除)
                string userId = _oAuth2ServerServices.GetUserIdFromCode(code);
                if (string.IsNullOrEmpty(userId))
                    return this.Json("code過期", JsonRequestBehavior.AllowGet);
                _oAuth2ServerServices.BingTokenAndUserId(access_token, userId);
                _oAuth2ServerServices.RemoveCode(code);

                //返回token
                return this.Json(access_token, JsonRequestBehavior.AllowGet);
            }
            //重新整理token
            else if (grant_type == "refresh_token")
            {
                //新建token
                string new_access_token = TokenCodeUtil.GetToken();
                //替換儲存新的token,預設是30分鐘

                //返回新建的token
                return this.Json(new_access_token, JsonRequestBehavior.AllowGet);
            }
            return this.Json("error grant_type=" + grant_type, JsonRequestBehavior.AllowGet);
        }

 

3.4通過token獲取使用者資訊

上面請求端已經獲取到了token,所以這裡只需要驗證token,token驗證通過就直接返回使用者資訊。

驗證token包括驗證是否存在、驗證是否過期。

        /// <summary>
        /// 通過token獲取使用者資訊
        /// </summary>
        /// <param name="oauth_token"></param>
        /// <returns></returns>
        public ActionResult UserInfo(string oauth_token)
        {
            if(!_oAuth2ServerServices.IsTokenValied(oauth_token, DateTime.Now))
                return this.Json("oauth_token無效", JsonRequestBehavior.AllowGet);
            UserInfo u = _oAuth2ServerServices.GetUserInfoFromToken(oauth_token);
            return this.Json(u, JsonRequestBehavior.AllowGet);
        }

 

4. 結語

到此,我們寫OAuth2.0服務端的程式碼已經結束了。

附上原始碼:https://github.com/cjt321/MyOAuth2Server

下一篇將介紹請求端怎麼請求我們的服務端,來測試流程、程式碼是否正確:http://www.cnblogs.com/alunchen/p/6957785.html

 

可以關注本人的公眾號,多年經驗的原創文章共享給大家。