.Net Core 集成JWT授权验证 从页面到API详解
1.什么是JWT
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。 JWT的官网地址:link. 通俗来讲,JWT定义了一种紧凑的、自包含的方式,可以将各方之间的信息作为JSON对象安全的传输,它是代表用户身份的一个象征令牌,可以在api接口中校验用户的身份以确认用户是否有访问api的权限。
2.JWT的使用场景
用户授权: 需要实现单点登录的时候可以使用,例如在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,一次登录能得到其他系统的信任。用户使用JWT授权登录成功后会返回一个token值给当前用户,用户访问其他的模块的时候携带该token进行请求,当token过期或被纂改,则不允许访问。 信息交换: JWT是服务器、客户端之间安全的进行传输信息的好方式。因为在颁发JWT的时候可以对JWT进行签名,多方之间可以通过约定好的加密秘钥进行数据解析。
3.JWT较之Session认证的区别
Session认证
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
基于token的鉴权机制
1、用户使用用户名密码来请求服务器。
2、服务器进行验证用户的信息。
3、服务器通过验证颁发给用户一个token。
4、客户端存储token,并在每次请求时附送上这个token值。
5、服务端验证token值,并返回数据。
基于Session认证模式的问题在于,扩展性不好,单机使用没有问题,如果使用到了服务器集群或者是跨域的服务导向架构,就会需要每台服务器都要能够读取到session。例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。用这种方案架构看上去清晰,但是工程量大。 而另外一种方案是不在服务器保存session数据,将数据保存在客户端,在用户发起的每次请求的时候都带到服务器,它不需要在服务端保留用户的会话状态信息,不用考虑在哪一台服务器进行登录,为应用的扩展提供了便利。但是JWT在Payload里面包含了附件信息,占用的空间比Session大,在http传输的过程中会造成性能影响。所以在设计的时候不要在JWT中存储太多的claim,避免发生巨大的请求。
4.JWT的结构
令牌由三部分组成,这些部分由 (.) 分隔开,分别是:
因此,JWT通常是这种形式使用 qqqqq.wwwww.eeeee 标题: 通常有两部分组成,令牌类型和使用的签名算法。
{
"alg": "HS256",
"typ": "JWT"
}
有效载荷: payload部分是一个json对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方定义的字段,还可以在这个部分定义自己的私有字段。
验证签名: 签名是将第一部分(header)、第二部分(payload)、密钥(key)通过指定算法(HMAC、RSA)进行加密生成的。 详细内容可看下图: 前言介绍完毕,接下来应用下如何获取JWT并用于访问API或服务器资源。流程如下:
- 应用程序向授权服务器请求授权。
- 校验用户身份,校验成功,返回token。
- 应用程序使用访问令牌访问受保护的资源。
5.Asp.Net Core 集成JWT
开发工具:vs2019
首先新建一个asp.net core的web项目,项目版本选择3.1及以上。 接下来在appsettings.json添加配置信息
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"JwtSettings": {
"Issuer": "https://localhost:51945",
"Audience": "https://localhost:51945",
"SecretKey": "Hello-key----------"
},
"TokenValidMinutes": "1",
"TokenCacheMinutes": "5"
}
Issuer是签发人,Audience是受众,使用者、信息传播的接受者。SecretKey是定义的秘钥。
TokenValidMinutes是自己定义的有效分钟数,
TokenCacheMinutes是自己定义的缓存分钟数,
然后去StartUp类中添加配置。 在ConfigureServices方法中添加代码如下:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
ValidIssuer = Appsettings.Issuer,
ValidAudience = Appsettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(Appsettings.SecretKey)),
};
});
在Configure方法中添加代码如下:
app.UseAuthentication();
在创建的项目中新建一个文件夹,里面建立一个控制器,层级关系如下图所示: 该控制器需要继承ActionFilterAttribute,控制器具体代码如下(上面的命名空间就不复制了vs可以直接导):
public class CheckJWTFilter : ActionFilterAttribute
{
public bool IsCheck { get; set; } = true;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
bool ignoreCheckSession = filterContext.ActionDescriptor.FilterDescriptors
.Select(f => f.Filter)
.OfType<TypeFilterAttribute>()
.Any(f => f.ImplementationType.Equals(typeof(IgnoreCheckJWTFilter)));
if (ignoreCheckSession)
{
base.OnActionExecuting(filterContext);
return;
}
if (IsCheck)
{
var claimIdentity = (ClaimsIdentity)filterContext.HttpContext.User.Identity;
if (claimIdentity.Claims.Count() > 0 && JWTUtil.ValidateLogin(claimIdentity))
{
base.OnActionExecuting(filterContext);
}
else
{
filterContext.Result = new UnauthorizedResult();
}
}
else
{
base.OnActionExecuting(filterContext);
}
}
}
接下来在Controllers文件夹中新建一个HomeController,创建一个简单的页面,页面上包含一个文本框和几个简单的按钮即可。
<input type="text" id="name" />
<input type="text" id="pwd" />
<button onclick="btnLogin()">登录</button>
<button onclick="logOut()">退出登录</button>
<button onclick="getUserInfo()">获取用户信息</button>
然后再创建一个User控制器,用于接收页面提交过来的请求。代码如下:
[Route("GetToken")]
[HttpGet]
public IActionResult GetToken(string data)
{
LoginViewModel model = JsonConvert.DeserializeObject<LoginViewModel>(data);
model.Id = "1";
model.Phone = "138****8521";
model.Password = "123";
ResponseResult responseResult = new ResponseResult();
responseResult.Success = true;
responseResult.Data = JWTUtil.GetToken(model);
return Ok(responseResult);
}
[Route("GetUserInfo")]
[HttpGet]
[CheckJWTFilter]
public IActionResult GetUserInfo()
{
var claimIdentity = (ClaimsIdentity)HttpContext.User.Identity;
string name = claimIdentity.FindFirst(JwtClaimTypes.Name).Value;
string phoneNumber = claimIdentity.FindFirst(JwtClaimTypes.PhoneNumber).Value;
string expirationTimeStamp = claimIdentity.FindFirst(JwtClaimTypes.Expiration).Value;
DateTime expiration = DateTimeUtil.Unix2Datetime(Convert.ToInt64(expirationTimeStamp));
int code = GetStatusCode(expiration, Appsettings.TokenCacheMinutes);
return new JsonResult(new ResponseResult() { Data = name + "用户资料" });
}
[CheckJWTFilter]
[HttpGet]
[Route("RefreshToken")]
public IActionResult RefreshToken()
{
string token = HttpContext.Request.Headers["Authorization"].ToString();
string[] tokenArray = token.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var claimIdentity = (ClaimsIdentity)HttpContext.User.Identity;
string expirationTimeStamp = claimIdentity.FindFirst(JwtClaimTypes.Expiration)?.Value;
if (string.IsNullOrEmpty(expirationTimeStamp))
{
return new JsonResult(new ResponseResult() { Success = false, Data = tokenArray.Length > 1 ? tokenArray[1] : tokenArray[0] });
}
DateTime expiration = DateTimeUtil.Unix2Datetime(Convert.ToInt64(expirationTimeStamp));
if (DateTime.Now > expiration && DateTime.Now <= expiration.AddMinutes(Appsettings.TokenCacheMinutes))
{
ResponseResult responseResult = new ResponseResult();
responseResult.Success = true;
responseResult.Data = JWTUtil.GetToken(claimIdentity);
return Ok(responseResult);
}
return new JsonResult(new ResponseResult() { Success = true, Data = tokenArray.Length > 1 ? tokenArray[1] : tokenArray[0], Code = 200 });
}
private int GetStatusCode(DateTime expiration, int tokenCacheMinutes)
{
if (expiration < DateTime.Now)
{
if (expiration.AddMinutes(tokenCacheMinutes) < DateTime.Now)
{
return 9002;
}
else
{
return 9001;
}
}
return 200;
}
在页面上需要通过ajax提交的方式,将前台输入的信息传输到api。
<script type="text/javascript">
$(document).ajaxSend(function (e, jqxhr, opt) {
jqxhr.setRequestHeader("Authorization", "Bearer " + sessionStorage.getItem("token"));
});
$(document).ajaxSuccess(function (event, jqxhr, opt) {
if (jqxhr.responseJSON.Success) {
handleStatusCode(jqxhr.responseJSON.Code);
}
});
function btnLogin() {
let name = $("#name").val();
let pwd = $("#pwd").val();
$.ajax({
type: "GET",
url: "/User/GetToken",
contentType: "application/json",
data: {
data: JSON.stringify({
loginName: name,
password: pwd
})
}
}).success(function (data) {
if (data.Success) {
sessionStorage.setItem("token", data.Data);
}
});
}
function logOut() {
sessionStorage.removeItem("token");
}
function getUserInfo() {
$.ajax({
type: "get",
url: "/User/GetUserInfo"
}).done(function (data) {
if (data.Success) {
console.log(data.Data);
} else {
alert(data.ErrorMsg);
}
});
}
</script>
我们启动程序,模拟一下应用场景。 我们先不传递账号密码等数据,直接提交,然后再将返回的token进行获取用户信息。 用户资料是空的,因为我们在生成token的时候,没有将文本框内的数据存储到有效载荷内容中去,所以我们调用查询用户资料接口时,根据Authorization传递过去的token参数进行解析,也是拿不到数据的。
接下来传递文本框数据再次进行解析,可以很清晰的从返回值中看到api能够从我们传递过去的token中解析出内容。
小结
使用JWT控制接口和服务器资源的访问,需要在接口上添加特性,表示需要有校验通过可用的令牌才能访问,这里只是简单的演示了一下生成jwt以及解析jwt,后期实战进阶的时候有很多地方需要补充以及完善。 例如token失效过期后的客户端的响应、如何强制在token未过期时让客户端的token失效、如何无刷新交换新的token等等。
感谢阅读,敬请斧正。
|