今天看到了博友对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>
代码其实都是多余的,在编写时可以按照这种安全机制,去书写自己的授权逻辑。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具