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);
源碼地址: