雪花

.net Core jwt策略参数

一、实现

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,然后在去比较

 

 角色都不需要

posted @ 2020-01-16 17:21  十色  阅读(1163)  评论(0编辑  收藏  举报