ASP.NET MVC使用JWT代替session,实现单点登陆
1. 什么是Token?
什么是token?token可以理解为是一种令牌,常用在计算机身份认证。在与服务器进行数据传输之前,会进行身份核验。
2. 什么是JWT?
什么是JWT? JWT是Json Web Token的简称,是一种Token的规范。就是一个加密后的字符串,组成部分为A.B.C。该字符串是由记录token的加密方式,字符串长度(A部分),基本的用户信息,载荷,签发人,过期时间等(B部分),以及A和B共同的加密部分(C部分)构成。
3. Token与Session比较
传统Session所暴露的问题
Session: 用户每次在计算机身份认证之后,在服务器内存中会存放一个session,在客户端会保存一个cookie,以便在下次用户请求时进行身份核验。但是这样就暴露了两个问题。第一个问题是,session是存储到服务器的内存中,当请求的用户数量增加时,会加重服务器的压力。第二个问题是,若是有多台服务器,而session只能存储到当前的某一台服务器中,这就不适用于分布式开发。
CSRF: Session是基于cookie来进行用户识别的,如果cookie被截获,用户就很容易受到跨站请求伪造攻击,本文暂时不考虑csrf(cross site request forgery)。
Token的验证机制
token的验证不需要在服务器端保留任何的用户信息,因此,当用户再客户端通过单点登陆后,可以访问多台服务器,利于分布式开发。而且token的是一串加密后的字符串,可以设置过期日期,不容易被仿造。
使用token,客户端和服务端的交互流程大致是如下:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
token可以存放在cookie中,也可以保存在请求头中,建议将token放到请求头中,并且token携带mac地址和机器名。
4. ASP.NET MVC如何使用jwt实现单点登陆
定义一个UserState类
namespace LYQ.TokenDemo.Models.Infrastructure { public class UserState { public string UserName { get; set; } public string UserID { get; set; } public int Level { get; set; } } }
定义一个AppManager类和TokenInfo类
public static UserState UserState { get { HttpContext httpContext = HttpContext.Current; var cookie = httpContext.Request.Cookies[Key.AuthorizeCookieKey]; var tokenInfo = cookie?.Value ?? ""; //token 解密 var encodeTokenInfo = TokenHelper.GetDecodingToken(tokenInfo); UserState userState = JsonHelper<UserState>.JsonDeserializeObject(encodeTokenInfo); return userState; } } public class TokenInfo { public TokenInfo() { iss = "LYQ"; iat = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; exp = iat + 300; aud = ""; sub = "LYQ.VIP"; jti = "LYQ." + DateTime.Now.ToString("yyyyMMddhhmmss"); } public string iss { get; set; } public double iat { get; set; } public double exp { get; set; } public string aud { get; set; } public double nbf { get; set; } public string sub { get; set; } public string jti { get; set; } }
定义JsonHelper
public class JsonHelper<T> where T : class { public static T JsonDeserializeObject(string json) { return JsonConvert.DeserializeObject<T>(json); } public static string JsonSerializeObject(object obj) { return JsonConvert.SerializeObject(obj); } }
在Home控制器中定义一个Login的方法
[HttpGet] [LYQ.TokenDemo.Models.CustomAttribute.Authorize(false)] public ActionResult Login() { return View(); } [HttpPost] [LYQ.TokenDemo.Models.CustomAttribute.Authorize(false)] public ActionResult Login(string account, string password) { if (account == "Tim" && password == "abc123") { var cookie = new HttpCookie(Key.AuthorizeCookieKey, TokenHelper.GenerateToken()); HttpContext.Response.Cookies.Add(cookie); return Json("y"); } else { var cookie = new HttpCookie(Key.AuthorizeCookieKey, ""); HttpContext.Response.Cookies.Add(cookie); return Json("n"); } }
生成token
使用NuGet,下载JWT.dll
namespace LYQ.TokenDemo.Models { public class TokenHelper { //jwt私钥,不能公布 private const string SecretKey = "LYQ.abcqwe123"; public static string GenerateToken() { var tokenInfo = new TokenInfo(); var payload = new Dictionary<string, object> { {"iss", tokenInfo.iss}, {"iat", tokenInfo.iat}, {"exp", tokenInfo.exp}, {"aud", tokenInfo.aud}, {"sub", tokenInfo.sub}, {"jti", tokenInfo.jti}, { "userName", "Tim" }, { "userID", "001" }, { "level",18} }; IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, SecretKey); return token; } public static string GetDecodingToken(string strToken) { try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var json = decoder.Decode(strToken, SecretKey, verify: true); return json; } catch (Exception) { return ""; } } } }
自定义身份认证
本文这里采取的是自定义的身份认证模式,自定义了一个AuthorizeAttribute。
namespace LYQ.TokenDemo.Models.CustomAttribute { public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter { public AuthorizeAttribute(bool _isCheck = true) { this.isCheck = _isCheck; } private bool isCheck { get; } public void OnAuthorization(AuthorizationContext filterContext) { var httpContext = filterContext.HttpContext; var actionDescription = filterContext.ActionDescriptor; if (actionDescription.IsDefined(typeof(AllowAnonymousAttribute), false) || actionDescription.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)) { return; } if (!isCheck) return; if (AppManager.UserState == null) { if (httpContext.Request.IsAjaxRequest()) { filterContext.Result = new JsonResult() { Data = new { Status = "Fail", Message = "403 Forbin", StatusCode = "403" }, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; } else { filterContext.Result = new RedirectResult(("/Home/Login")); } } else { //每次身份验证通过后,重新响应一个新的token给客户端 var cookie = new HttpCookie(Key.AuthorizeCookieKey, TokenHelper.GenerateToken()); filterContext.HttpContext.Response.Cookies.Add(cookie); } } } }
HTML页面
@{ ViewBag.Title = "Login"; } <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <h2>This is login page.</h2> <div class="container"> <form class="box-body" action="/Home/Login" method="post"> <div class="form-group row"> <label class="col-sm-1 col-md-1">Account:</label> <div class="col-sm-5 col-md-5"> <input type="text" class="form-control" id="account" name="account" /> </div> </div> <div class="form-group row"> <label class="col-sm-1 col-md-1">Password:</label> <div class="col-sm-5 col-md-5"> <input type="password" class="form-control" id="password" name="password" /> </div> </div> <div class="form-group row"> <div class="col-sm-1 col-md-1"></div> <div class="col-sm-5 col-md-5"> <button type="button" class="btn btn-info" onclick="Login();">Login</button> <button type="reset" class="btn btn-info">Reset</button> </div> </div> <div class="form-group row"> <div class="col-sm-1 col-md-1"></div> <div class="col-sm-5 col-md-5"> <span>account:Tim; password:abc123</span> </div> </div> </form> </div> <script src="~/Scripts/jquery-3.3.1.min.js"></script> <script src="~/StaticFiles/Frontend/Scripts/Common.js"></script> <script> function Login() { var paras = { account: $("#account").val(), password: $("#password").val() }; LYQ.sendAjaxRequest({ type: "post", url: "/Home/Login", param: paras, dataType: "json", callBack: function (result) { if (result == "y") { console.log("Login success"); alert("Login success"); window.location = "/"; } else { console.log("Login fail"); alert("Login fail"); } } }); } </script>
Common.js
!(function (window) { var functions = { sendAjaxRequest: function (opts) { var self = this; $.ajax({ type: opts.type || "post", url: opts.url, data: opts.param || {}, contentType: opts.contentType === null ? true : opts.contentType, cache: opts.cache === null ? true : opts.cache, processData: opts.processData === null ? true : opts.processData, beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader(LYQ.getAuthorizationKey(), ""); }, dataType: opts.dataType || "json", success: function (result) { if (Object.prototype.toString.call(opts.callBack) === "[object Function]") { //判断callback 是否是 function opts.callBack(result); } else { console.log("CallBack is not a function"); } } }); }, getRequestHeaderAuthorizationToken: function () { var document_cookie = document.cookie; //var reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)"); //if (document_cookie = document.cookie.match(reg)) // return unescape(arr[2]); //else // return null; console.log(document_cookie); return document_cookie; }, getAuthorizationKey: function () { return 'Authorization'; } }; window.LYQ = functions; })(this);
源码地址: