今天看到了博友对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();//获取十一位唯一码
//改用Session保存
System.Web.HttpContext.Current.Application[tem_token] = source;
//写入有效期
System.Web.HttpContext.Current.Application["temtokenLimit"] = DateTime.Now.AddMinutes();
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(, ).ToLower().IndexOf("http") == -)
{
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(""))
{
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();//获取十四位唯一码
//验证是否已授权
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(, , , , , , );//计算时间戳
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(, , , , , , 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>
代码其实都是多余的,在编写时可以按照这种安全机制,去书写自己的授权逻辑。