ASP.NET MVC使用JWT代替session,实现单点登陆
- 1. 什么是Token?
- 2. 什么是JWT?
- 3. Token与Session比较
- 4. ASP.NET MVC如何使用jwt实现单点登陆
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);
源码地址:
- Github:https://github.com/Lyq1454759684/Token-Demo
- 码云:https://gitee.com/Tim_Lee_ZhongShan/Token-Demo