如果對OAuth2.0有任何的疑問,請先熟悉OAuth2.0基礎的文章:http://www.cnblogs.com/alunchen/p/6956016.html
1. 前言
本篇文章時對 客戶端的授權模式-授權碼模式 的創建,當然你理解的最復雜的模式之后,其他模式都是在授權碼模式上面做一些小改動即可。對於授權碼模式有任何的疑問,請看上面提到的文章。
注意:本文是創建OAuth的Server端,不是Client請求端。
2. 開始授權碼模式的概念、流程
第2點其實就是復制了上一篇文章,為了提高閱讀性,讀過上一篇文章的可略過第2點。
授權碼模式(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頭信息中明確指定不得緩存。
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
可以關注本人的公眾號,多年經驗的原創文章共享給大家。


