最近研究了一段时间的.net core整合jwt,感觉比较人性化的解决方案应该是当jwt过期时,应判断过期了多久,如果过期的时间超过某个值,那么则直接返回提醒jwt过期,需要登录,如果没有超过,则直接刷新,提高用户体验的同时,也可以保障一定的安全性。
本项目基于.net core5.0,开发工具vs2019,本文只罗列核心部分,详细源码详见
首先配置jwt,引用的依赖如下图:
以上两个都有引用,jwtBearer主要用于整合.net core,jwt.net用户对前端传入的token进行检查。
StartUp.cs中配置jwt核心代码如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(o=> {
o.Filters.Add<SkipAttribute>();//这个用户标识哪些类需要验证jwt
})
//以下语句要求.net core对NewtonsoftJson的支持,这样就可以像以前的web API一样调用了
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
//注册过滤器
Action<MvcOptions> filters = new Action<MvcOptions>(r => {
r.Filters.Add(typeof(MyFilters));
r.Filters.Add(typeof(MyExceptionFilterAttribute));
});
services.AddMvc(filters) //注册全局过滤器
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
//添加jwt支持
services.AddAuthentication
(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(j =>
{
j.RequireHttpsMetadata = false;
j.SaveToken = true;
j.TokenValidationParameters = new TokenValidationParameters
{
//是否调用对签名securityToken的SecurityKey进行验证
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtTokenString)),//签名秘钥
ValidateIssuer = true,//是否验证颁发者
ValidIssuer = "Issuer",//颁发者
ValidateAudience = true,//是否验证接受者
ValidAudience = "Audience",//接收者
ValidateLifetime = true,//是否验证失效时间
ClockSkew = TimeSpan.Zero,//这句话表示设置过期缓冲时间为0,过期时间一到立即失效
RequireExpirationTime = true
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//启用静态文件支持,这个必须放在app.UseRouting()上
app.UseStaticFiles();
app.UseRouting();
//添加Cors中间件
app.UseCors(corsPolicyName);
//身份认证中间件
app.UseAuthentication();
//授权中间件
app.UseAuthorization();
//app.UseExceptionHandler();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
下面说下程序运行流程,
用户在前端发送ajax请求,先到达拦截器(过滤器),核心检查都在拦截器中判断,判断是否过期,未过期则放行,过期了,是否未超过1个小时,则刷新token后放行,超过了,则拦截,返回token过期给前端
拦截器核心代码
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using NetCore5.Utils;
using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace NetCore5.Filter
{
//优先级1:权限过滤器IAuthorizationFilter:它在Filter Pipleline中首先运行,并用于决定当前用户是否有请求权限。如果没有请求权限直接返回。
public class MyFilters : IAuthorizationFilter
{
[Obsolete]
public void OnAuthorization(AuthorizationFilterContext context)
{
//判断是否需要校验
var isDefined = false;
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (controllerActionDescriptor != null)
{
isDefined = controllerActionDescriptor.MethodInfo.GetCustomAttributes(inherit: true)
.Any(a => a.GetType().Equals(typeof(SkipAttribute)));
}
//是已经标记的不需要检查token的类,则跳过检查
if (isDefined)
{
return;
}
var token = context.HttpContext.Request.Headers["Authorization"].ToString(); //ajax请求传过来
string pattern = "^Bearer (.*?)$";
if (!Regex.IsMatch(token, pattern))
{
context.Result = new ContentResult { StatusCode = 401, Content = "token格式不对!格式为:Bearer {token}" };
return;
}
token = Regex.Match(token, pattern).Groups[1]?.ToString();
if (token == "null" || string.IsNullOrEmpty(token))
{
context.Result = new ContentResult { StatusCode = 401, Content = "token不能为空" };
return;
}
//校验auth的正确性
var result = TokenHelper.JWTJieM(token);
//过期了
if (result == "expired")
{
// context.Result = new ContentResult { StatusCode = 401, Content = "登录已过期" };
// return;
//继续判断是否已经过期超过1个小时
//解析payload,拿到exp
var exp = TokenHelper.GetExp(token);
//解析exp具体时间
DateTime expTime = DateTimeUtil.TimeStampToDateTime(long.Parse(exp));
//跟当前时间比较,有没有超过1个小时,不超过则刷新,超过则返回token过期,需要重新登录
bool isExpire = DateTimeUtil.DiffMin(expTime);
if (isExpire)
{
context.Result = new ContentResult { StatusCode = 401, Content = "登录已过期,请重新登录" };
return;
}
else
{
//刷新token
token = TokenHelper.refreshToken(token);
}
}
else if (result == "invalid")
{
context.Result = new ContentResult { StatusCode = 401, Content = "非法操作" };
return;
}
else if (result == "error")
{
context.Result = new ContentResult { StatusCode = 401, Content = "出错啦!" };
return;
}
}
}
//优先级2:资源过滤器: 它在Authorzation后面运行,同时在后面的其它过滤器完成后还会执行。
//Resource filters 实现缓存或其它性能原因返回。因为它运行在模型绑定前,所以这里的操作都会影响模型绑定。
//注意,异常处理未验证,需要读者自己实践
//优先级4:异常过滤器:被应用全局策略处理未处理的异常发生前异常被写入响应体
public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IModelMetadataProvider _moprovider;
public MyExceptionFilterAttribute(IModelMetadataProvider moprovider)
{
this._moprovider = moprovider;
}
public override void OnException(ExceptionContext context)
{
base.OnException(context);
if (!context.ExceptionHandled)//如果异常没有被处理过
{
string controllerName = (string)context.RouteData.Values["controller"];
string actionName = (string)context.RouteData.Values["action"];
//string msgTemplate =string.Format( "在执行controller[{0}的{1}]方法时产生异常",controllerName,actionName);//写入日志
if (this.IsAjaxRequest(context.HttpContext.Request))
{
context.Result = new JsonResult(new
{
Result = false,
PromptMsg = "系统出现异常,请联系管理员",
DebugMessage = context.Exception.Message
});
}
else
{
var result = new ViewResult { ViewName = "~Views/Shared/Error.cshtml" };
result.ViewData = new ViewDataDictionary(_moprovider, context.ModelState);
result.ViewData.Add("Execption", context.Exception);
context.Result = result;
}
}
}
//判断是否为ajax请求
private bool IsAjaxRequest(HttpRequest request)
{
string header = request.Headers["X-Requested-With"];
return "XMLHttpRequest".Equals(header);
}
}
public class SkipAttribute : Attribute, IFilterMetadata
{
///允许未通过身份验证也可以访问的特性
}
}
到此,核心代码介绍完毕,详细源码请见开头的下载链接