一、实现
1、Permission文件
代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// 用户或角色或其他凭据实体 /// </summary> public class Permission { /// <summary> /// 用户或角色或其他凭据名称 /// </summary> public virtual string RoleName{ get; set; } /// <summary> /// 请求Url /// </summary> public virtual string Url{ get; set; } } }
如图所示:
2、PermissionHandler.cs
代码如下:
using Blog.Jwt; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// 权限授权Handler /// </summary> public class PermissionHandler : AuthorizationHandler<PermissionRequirement>//我们自定义用户属性,并自己验证 { /// <summary> /// 验证方案提供对象 /// </summary> public IAuthenticationSchemeProvider Schemes { get; set; } private readonly IHttpContextAccessor _accessor; /// <summary> /// 构造函数注入 /// </summary> /// <param name="schemes"></param> /// <param name="accessor"></param> public PermissionHandler(IAuthenticationSchemeProvider schemes, IHttpContextAccessor accessor) { Schemes = schemes; _accessor = accessor; } /// <summary> /// 摘要:根据特定需求决定是否允许授权 特定就是add参数,但是未包含在创建token属性内 /// /// 二点注意: /// 一、没必要重写加参数,我们可以从上下文中获取到token(老张完全多此一句) 我们完全可以从token获取用户信息并对比 requirement完全多余 /// 二、HandleRequirementAsync用了 这个方法 没有办法HttpContext.Response.WriteAsync,不然会报错报错{StatusCode cannot be set because the response has already started.}因为响应已经开始 /// 所以我们尽量不在此方法里面写,我们只需要知道时间过期则请求头部 context.Response.Headers.Add("Token-Expired", "true"); 即可,其他一律为未授权 /// 其实我很想 提示 未授权和认证上失败的 区分下 /// /// 第二次回顾 /// 1、具体提示信息既然无法返回 body内容,就返回状态码(这个是可行的),有个全局拦截来根据返回自定义状态码处理,状态码状态信息是对外一致(也是全局拦截的统一状态码信息的好处) /// 2、我之前认为说为什么add参数呢,甚至你不用系统提供的auth,直接写个中间件一样能实现。微软这么设计,是为了更复杂的抽象层而已。 /// /// 我还是选择不继承 AuthorizationHandler ,不添加add 参数,直接用 /// /// reqirement 用来定义授权认证的参数 拿他来验证的 /// /// </summary> /// <param name="context"></param> /// <param name="requirement"></param> /// <returns></returns> protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { #region 想返回具体提示 /* if (!_accessor.HttpContext.Response.HasStarted)//先判断context.Response.HasStarted { var results = JsonConvert.SerializeObject(new ApiResultModels { Success = false, Message = "测试返回结果", Code = "406" }); await _accessor.HttpContext.Response.WriteAsync(results); //写入后报错{StatusCode cannot be set because the response has already started.}因为响应已经开始 if (true) { context.Fail(); return CompletedTask; } } //想返回想要的返回具体结果比如 过期时间提示token已过期、Audience颁布者则提示token无效! 只能通过设置状态码解决 _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;//403 context.Fail(); if (true) */ #endregion ////赋值用户权限 //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息 //var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; var httpContext = _accessor.HttpContext; /*//动态获取权限项目 if (!requirement.Permissions.Any()) { var data = await _role.GetPermissions(); var list = (from item in data where item.IsDeleted = false orderby item.Id select new Permission { Url = "", Name = "" }).ToList(); requirement.Permissions = list; }*/ //请求Url var questUrl = httpContext.Request.Path.Value.ToLower(); #region 判断请求是否停止 //判断请求是否停止 var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();// IAuthenticationSchemeProvider 用来提供对Scheme的注册和查询 foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())//按请求处理的优先级顺序返回方案(Scheme)。 { //来获取指定的Scheme的Hander var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) //handler.HandleRequestAsync 如果请求处理应停止,则返回true { context.Fail(); return; } } //后台 怎么根据 HttpContext判断请求是否停止?(保留疑问) #endregion //判断请求是否拥有凭据,即有没有登录 GetDefaultAuthenticateSchemeAsync 是获取默认的授权方案信息 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();//public IAuthenticationSchemeProvider Schemes 验证方案提供对象 1、获取认证方案实例对象不为null if (defaultAuthenticate == null) throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); else { //扩展方法 public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) 2、context上下文通过身份验证方案的名称,获取认证方案信息 //判断使用是否授权 var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);//身份认证 defaultAuthenticate.Name==Bearer(header里面有Bearer) //result?.Principal不为空即登录成功 if (result?.Principal != null) { httpContext.User = result.Principal;//3、token字符串 var strToken = result.Properties.Items.FirstOrDefault().Value;//token字符串 //权限中是否存在请求的url if (requirement.Permissions.GroupBy(g => g.Url).Where(w => w.Key?.ToLower() == questUrl).Count() > 0) { // 获取当前用户的角色信息 var currentUserRoles = (from item in httpContext.User.Claims where item.Type == requirement.ClaimType select item.Value).ToList(); //验证权限 失败则 if (currentUserRoles.Count <= 0 || requirement.Permissions.Where(w => currentUserRoles.Contains(w.RoleName) && w.Url.ToLower() == questUrl).Count() <= 0) { // 可以在这里设置跳转页面,不过还是会访问当前接口地址的 //httpContext.Response.Redirect(requirement.DeniedAction); _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;//401 未授权 context.Fail(); //if (true) return; } } else { //context.Fail(); //return; _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;//404 此url不存在 context.Fail(); //if (true) return; } //判断过期时间 if ((httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) != null && DateTime.Parse(httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.UtcNow) { context.Succeed(requirement); } else { context.Fail(); return; } return; } else { _accessor.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;//403 禁止访问 context.Fail(); //if (true) return; } } //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败 if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)) { context.Fail(); return; } context.Succeed(requirement); } } }
3、PermissionRequirement.cs如下
代码如下:
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Blog.Jwt { /// <summary> /// https://blog.csdn.net/qq_25086397/article/details/103765090 /// 必要参数类 继承 IAuthorizationRequirement 方便从PermissionRequirement将属性取出赋值道PermissionRequirement类成员上 /// </summary> public class PermissionRequirement : IAuthorizationRequirement { /// <summary> /// 用户权限集合 /// </summary> public List<Permission> Permissions { get; private set; } /// <summary> /// 无权限action /// </summary> public string DeniedAction { get; set; } /// <summary> /// 认证授权类型 /// </summary> public string ClaimType { internal get; set; } /// <summary> /// 请求路径 /// </summary> public string LoginPath { get; set; } = "/Api/Login"; /// <summary> /// 发行人 /// </summary> public string Issuer { get; set; } /// <summary> /// 订阅人 /// </summary> public string Audience { get; set; } /// <summary> /// 过期时间 /// </summary> //public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5000); public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(1); /// <summary> /// 签名验证 /// </summary> public SigningCredentials SigningCredentials { get; set; } /// <summary> /// 构造函数 /// </summary> /// <param name="deniedAction">拒约请求的url</param> /// <param name="permissions">权限集合</param> /// <param name="claimType">声明类型</param> /// <param name="issuer">发行人</param> /// <param name="audience">订阅人</param> /// <param name="signingCredentials">签名验证实体</param> public PermissionRequirement(string deniedAction, List<Permission> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials) { ClaimType = claimType; DeniedAction = deniedAction; Permissions = permissions; Issuer = issuer; Audience = audience; SigningCredentials = signingCredentials; //DI容器,注册到容器内,此时无New实例化,仅构造函数用到的时候才会new,内存才会有该实例对象 /*//没有权限则跳转到这个路由 DeniedAction = new PathString("/api/nopermission"); //用户有权限访问的路由配置,当然可以从数据库获取 Permissions = new List<Permission> { new Permission { Url="/api/value3", Name="admin"}, };*/ } } }
JwtMiddlewareExtensions.cs
using Common; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; namespace Blog.Jwt { //1、自定义授权策略验证 2、颁发者token和刷新token验证 public static class JwtMiddlewareExtensions { public static IServiceCollection AddJwtMiddleware(this IServiceCollection services, IConfiguration Configuration) { #region 注册Jwt验证 //在ConfigureServices中注入验证(Authentication),授权(Authorization),和JWT(JwtBearer) var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"])); //↓Token 的信息配置 var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true,////是否验证SecurityKey IssuerSigningKey = signingKey,//拿到SecurityKey ValidateIssuer = true,//是否验证Issuer ValidIssuer = Configuration["Jwt:Issuer"],//Issuer,这两项和前面签发jwt的设置一致 ValidateAudience = false,//是否验证Audience //为了验证token和刷新token两套Audience,后续处理 ValidAudience = Configuration["Jwt:Audience"],//Audience,这两项和前面签发jwt的设置一致 ValidateLifetime = true,//是否验证超时 当设置exp和nbf时有效 同时启用ClockSkew ClockSkew = TimeSpan.Zero //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了 //https://www.cnblogs.com/7tiny/p/11019698.html /*,AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); },*/ }; // var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); //这个集合模拟用户权限表,可从数据库中查询出来 // 如果要数据库动态绑定,这里先留个空,后边处理器里动态赋值 var permission = new List<Permission> { new Permission { Url="/", RoleName="Admin"}, new Permission { Url="/api/home/values", RoleName="Admin"}, new Permission { Url="/", RoleName="SysTem"}, new Permission { Url="/api/home/values1", RoleName="Admin"}, new Permission { Url="/api/home/values2", RoleName="Admin"}, new Permission { Url="/api/home/values1", RoleName="SysTem"}, new Permission { Url="/api/home/values2", RoleName="SysTem"} }; //var permission = new List<Permission>(); //New一个PermissionRequirement实体类,是为了从扩展额外参数,先注入,然后在PermissionHandler验证使用这些参数,这个参数我们自己定义的,方便验证的时候使用 //如果第三个参数,是ClaimTypes.Role,上面集合的每个元素的Name为角色名称,如果ClaimTypes.Name,即上面集合的每个元素的Name为用户名 var permissionRequirement = new PermissionRequirement("/api/denied", permission, ClaimTypes.Role, Configuration["Jwt:Issuer"], Configuration["Jwt:Audience"], signingCredentials); services.AddAuthorization(options => //↓导入角色身份授权策略 { options.AddPolicy("Permission", policy => policy.Requirements.Add(permissionRequirement)); }).AddAuthentication(options => //↓身份认证类型 { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o =>//↓Jwt 认证配置 { //不使用https o.RequireHttpsMetadata = false; o.TokenValidationParameters = tokenValidationParameters; o.Events = new JwtBearerEvents { //此处为权限验证失败后触发的事件 OnChallenge = context => { //此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须 context.HandleResponse(); //自定义自己想要返回的数据结果,我这里要返回的是Json对象,通过引用Newtonsoft.Json库进行转换 var result = new ApiResultModels(); result.Code = false; result.Message = "很抱歉,您无权访问该接口!"; //自定义返回的数据类型 context.Response.ContentType = "application/json"; //自定义返回状态码,默认为401 我这里改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //context.Response.StatusCode = StatusCodes.Status401Unauthorized; //输出Json数据结果 context.Response.WriteAsync(JsonConvert.SerializeObject(result)); return Task.FromResult(0); } ,OnAuthenticationFailed = context => { //如果过期,则把<是否过期>添加到,返回头信息中 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); //注入授权Handler services.AddScoped<IAuthorizationHandler, PermissionHandler>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton(permissionRequirement); #endregion return services; } } }
方便在Startup.cs使用
二、 只验证 角色的
PermissionRequirement requiremen 相当于new 一个实体吧(虽然以前是构造方法的写法)
从数据库查出权限列表给requiremen,然后在去比较
角色都不需要