今天看到了博友對SSO的文章,SSO單點登錄的講解突然想寫一篇關於OAuth2.0用戶授權的介紹。
應用場景:在APP或者網頁接入一些第三方應用時,時長會需要用戶登錄另一個合作平台,比如QQ,微博,微信的授權登錄。
使用好處:這樣可以免去用戶同步的麻煩,同時也增加了用戶信息的安全。
交互模型:
1.接口需要經過“1 次認證+1 次授權+1 次審核” 即可獲得 accesstoken,請求業 務模式說明:
1.1 來源認證: 用戶訪問您的移動應用(網頁),請根據確認合作名稱(例如: 金融網)作為 來源認證,服務端驗證無誤后會返回一個臨時令牌( 有效期 30 分鍾)。開發授權的話這一步就可以忽略來源認證
1.2 用戶授權: 接收到臨時令牌加入本次請求回調地址( 回調地址格式: https:// 或 http://外網可訪問網址), 用戶登錄授權通過后將會在回調地址中返回授權通 過碼( 參數名: code)。
1.3 令牌審核: 第一步訪問得到臨時令牌,第二次請求得到授權通過碼, 授權通過碼加臨時令牌通過后將會返回 AccessToken( 注意臨時令牌有效時間 30 分鍾)。
臨時令牌的時間可以根據實際情況設置
第一步獲取臨時令牌
// // GET: /API2/UserAuth/ /// <summary> /// 獲取臨時令牌 /// </summary> /// <param name="source">請求來源</param> /// <returns> /// tem_token:xxxxx,//臨時令牌(有效時間30分鍾) /// </returns> public ActionResult GetTemToken(string source) { if (source.Equals("XX來源")) { var tem_token = "jxbtem_" + Tools.GetRandomString(11);//獲取十一位唯一碼 //改用Session保存 System.Web.HttpContext.Current.Application[tem_token] = source; //寫入有效期 System.Web.HttpContext.Current.Application["temtokenLimit"] = DateTime.Now.AddMinutes(30); return Tools.GetResult(new { tem_token }); } else { return Tools.GetResult("未知請求來源", null); } }
第二步使用臨時令牌用戶授權
/// <summary> /// 使用臨時令牌用戶授權 /// </summary> /// <param name="callbackurl">回調地址</param> /// <param name="temtoken">臨時令牌</param> /// <returns> /// 驗證通過跳轉用戶授權,驗證失敗顯示失敗原因 /// </returns> public ActionResult UserAuthorization(string callbackurl, string temtoken) { //校驗參數合法性 if (string.IsNullOrEmpty(callbackurl) || string.IsNullOrEmpty(temtoken)) { return Tools.GetResult("非法請求"); } if (callbackurl.Trim().Substring(0, 4).ToLower().IndexOf("http") == -1) { return Tools.GetResult("回調地址不合法"); } //傳遞重要參數 object session = System.Web.HttpContext.Current.Application[temtoken]; object temtokenLimit = System.Web.HttpContext.Current.Application["temtokenLimit"]; if (session != null && temtokenLimit != null) { Session["callbackurl"] = callbackurl; Session["temtokenLimit"] = temtokenLimit; Session["temtoken"] = temtoken; Session["source"] = session; System.Web.HttpContext.Current.Application.Remove(temtoken); System.Web.HttpContext.Current.Application.Remove("temtokenLimit"); return RedirectToAction("GotoUserAut"); } else { System.Web.HttpContext.Current.Application.Remove(temtoken); System.Web.HttpContext.Current.Application.Remove("temtokenLimit"); return Tools.GetResult("令牌驗證失敗"); } }
第三步跳轉用戶授權頁面
/// <summary> /// 跳轉用戶授權頁面 /// </summary> /// <returns></returns> public ActionResult GotoUserAut(string MSG) { //驗證授權信息 if (!string.IsNullOrEmpty(MSG)) { ViewBag.MSG = MSG; return View(); } //獲取用戶信息 string uname = Request["uname"]; string pwd =Request["pwd"]; string md5 = Request["md5"]; ViewBag.MSG = ""; var source = Session["source"]; ViewBag.Source = source; if (string.IsNullOrEmpty(uname) || string.IsNullOrEmpty(pwd)) { ViewBag.MSG = "請輸入用戶名密碼"; return View(); } if (string.IsNullOrEmpty(md5) || md5.Equals("0")) { pwd = ToolKit.EncryptMd5(pwd); } //驗證用戶信息 if (!string.IsNullOrEmpty(uname) && !string.IsNullOrEmpty(pwd)) { var basma = DBHelper.BASMA.FirstOrDefault(ma => ma.MA001.Equals(uname) && ma.MA002.Equals(pwd)); if (basma != null) { return View(basma); } else { ViewBag.MSG = "用戶名或密碼錯誤"; return View(); } } return View(); }
第四步確認授權登錄
/// <summary> /// 確認授權登錄 /// </summary> /// <param name="UserId">用戶ID</param> /// <returns> /// 跳轉第三方回調頁面 /// </returns> public ActionResult StartUserAuthorization(int UserId) { BASMA UserInfo = null; if (UserExist(UserId, out UserInfo)) { var source = Session["source"]; var callbackurl = Session["callbackurl"]; var temtoken = Session["temtoken"]; var temtokenLimit = Session["temtokenLimit"]; if (source==null|| callbackurl==null || temtoken==null || temtokenLimit==null) { return RedirectToAction("GotoUserAut", new { MSG = "授權信息驗證失敗" }); } var accesstoken = "jxb_" + Tools.GetRandomString(14);//獲取十四位唯一碼 //驗證是否已授權 var basau = DBHelper.BASAU.FirstOrDefault(au => au.AU001.Equals(UserId) && au.AU002.Equals(source.ToString())); if (basau == null) { BASAU newbasau = new BASAU(); newbasau.AU001 = UserId; newbasau.AU002 = source.ToString(); newbasau.AU003 = temtoken.ToString(); newbasau.AU004 = DateTime.Parse(temtokenLimit.ToString()); newbasau.AU005 = accesstoken; newbasau.AU006 = DateTime.Now; DBHelper.BASAU.InsertOnSubmit(newbasau); DBHelper.SubmitChanges(); } else { basau.AU003 = temtoken.ToString(); basau.AU004 = DateTime.Parse(temtokenLimit.ToString()); basau.AU005 = accesstoken; basau.AU006 = DateTime.Now; DBHelper.SubmitChanges(); } //計算回調返回code TimeSpan ts = (basau.AU006??DateTime.Now) - new DateTime(1970, 1, 1, 0, 0, 0, 0);//計算時間戳 string TimeSpan = Convert.ToInt64(ts.TotalMilliseconds).ToString(); //獲得時間戳 return Redirect(callbackurl + "?code=" + TimeSpan);//回調返回授權碼 } else { return RedirectToAction("GotoUserAut", new { MSG = "指定授權用戶不存在" }); } }
第五步獲取應用授權令牌
/// <summary> /// 獲取應用授權令牌 /// </summary> /// <param name="code">授權成功返回碼</param> /// <param name="temtoken">請求臨時令牌</param> /// <returns> /// accesstoken:xxxxx,//授權碼 /// </returns> public ActionResult GetAccessToken(string code, string temtoken) { if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(temtoken)) { return Tools.GetResult("請求參數不能為空", null); } DateTime dtBase = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); DateTime convertTime = dtBase.Add(new TimeSpan(long.Parse(code) * TimeSpan.TicksPerMillisecond)); var BASAU = DBHelper.BASAU.FirstOrDefault(au => au.AU006.Equals(convertTime) && au.AU003.Equals(temtoken)&&au.AU004.Value>=DateTime.Now); if (BASAU != null) { return Tools.GetResult(new { accesstoken = BASAU.AU005 }); } else { return Tools.GetResult("令牌驗證失敗", null); } }
第六步獲取用戶唯一標識
/// <summary> /// 獲取用戶唯一標識 /// </summary> /// <param name="accesstoken">授權令牌</param> /// <returns> /// MA099:xxxxx,//用戶唯一標識 /// MA010:xxxxx,//用戶昵稱 /// </returns> public ActionResult GetUserInfo(string accesstoken) { if (!string.IsNullOrEmpty(accesstoken)) { var basau = DBHelper.BASAU.FirstOrDefault(ua => ua.AU005.Equals(accesstoken)); if (basau != null) { var basma = DBHelper.BASMA.FirstOrDefault(ma => ma.ID.Equals(basau.AU001)); if (basma != null) { return Tools.GetResult(new { basma.MA099, basma.MA010 }); } else { return Tools.GetResult("用戶信息拉取失敗", null); } } else { return Tools.GetResult("令牌驗證失敗", null); } } else { return Tools.GetResult("請求參數非法",null); } }
授權是需要用戶登錄才能授權,如果在自己的應用內或者可以提供用戶標識就可以直接通過。
附上完整的授權頁面代碼:
@model Ecio_Admin.Models.BASMA @{ ViewBag.Title = "GotoUserAut"; Layout = null; //獲取用戶ID var UserId = ""; if (Model != null) { UserId = Model.ID.ToString(); } } <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <meta charset="UTF-8"> <title>應用授權</title> <link href="~/Content/css/UserAut.css" rel="stylesheet" /> <script src="~/Scripts/jquery-1.10.2.min.js"></script> <script type="text/javascript"> $(function () { //授權 $("#userauth").click( function () { location.href = "/API2/UserAuth/StartUserAuthorization?UserId=" + $("#UserId").val(); }); }); </script> </head> <body> <div class="box"> <input type="hidden" id="UserId" value="@UserId" /> <img class="logo" src="~/UploadFiles/IMG/OAuth.png" alt="LOGO" /> @{ if (Model != null) { <h1 class="title">登錄后該應用將獲得以下授權:</h1> <h2 class="title2"><input type="checkbox" checked readonly="readonly" disabled="disabled">將獲取您的基本信息(昵稱、頭像)</h2> <form class="form-group" action="#" method="post"> <input class="form-btn" style="cursor:pointer;" type="button" id="userauth" value="確定授權" /> </form> } else { <h1 class="title">授權<span>@ViewBag.Source</span>訪問你的XXX賬號</h1> <form class="form-group" action="/API2/UserAuth/GotoUserAut" method="post"> <input class="form-import" type="text" placeholder="請輸入您的賬號" id="uname" name="uname" required autocomplete="off" /> <input class="form-import" type="password" placeholder="請輸入您的密碼" id="pwd" name="pwd" required autocomplete="off" /> <div style="color:red;">@ViewBag.MSG</div> <input class="form-btn" style="cursor:pointer;" type="submit" id="Login" value="登錄" /> </form> } } </div> </body> </html>
代碼其實都是多余的,在編寫時可以按照這種安全機制,去書寫自己的授權邏輯。
