从壹开始先后端分离[.NetCore] 37 ║JWT完美实现权限与接口的动态分配

缘起

本文已经有了对应的管理后台,地址:https://github.com/anjoy8/Blog.Adminhtml

哈喽你们好呀!又过去一周啦,这些天小伙伴们有没有学习呀,已经有一周没有更新文章了,不过晚上的时候,我也会看一些书和资料,这里给你们分享下:git

一、以前简单的写了一个DDD+CQRS+ES的第二个系列《D3模式设计初探 与 个人计划书》,已经基本完结了,写的比较简单,而后我也找到了微软的一个官方的一个资料《CQRS Journey》,不知道有没有哪位小伙伴看,全英文的,我还在看,由于官方已经不翻译了(更正:楼下有小伙伴评论,已经有这本书了,《探索CQRS和事件源(微软云计算系列丛书)》,先买了看看,好了再反馈),因此我打算本身翻译下,若是有想和我一块儿的小伙伴,能够留言,我们成立一个小组,一块儿翻译这个资料,主要是关于CQRS读写分离的和ES事件溯源的,固然是基于DDD领域驱动设计架构的基础上,有助于本身的理解。github

二、而后就是在看《IdentityServer4.Samples》,是一个IS4的官方栗子,而后找了找资料,由于个人第三个系列教程 —— 是想作一个权限管理系统(这里是打算搭建一个 IdentityServer4 + .net core Identity + EFCore + VueAdmin )的一个先后端分离的权限管理框架,初步打算是基于按钮级别的权限控制,从部门到岗位,角色到用户等都动态可控(保存到数据库,能够管理后台修改),开笔时间尚未定,由于还在学习和年末公司总结了,若是有小伙伴想一块儿开发,能够看看上边的这些技术,我们之后能够合做,为.net 开源社区作贡献(这里说下是彻底无偿的哟)。web

好啦,废话很少说,由于今天是不按期更新系列,因此会之间进入主题,不会有概念性的讲解,立刻开始今天的内容啦!主要是如下几个方面:sql

重要必看!刚刚下边评论有大神提出异议,我表示他说的有道理,如下内容,本身练习玩玩儿便可,固然小项目也可使用,不过中型以上的项目,仍是要用IdentityServer4这种成熟的轮子,本文只是一个小思考,还有不少地方值得推敲和商榷,不过这不就是学习的目的么,发表思想,提出异议,作出解决,加油!数据库

一、实现角色和接口API保存到数据库,实现动态分配json

二、接口中,以前的受权方法依然保留,在 BlogController.cs 中仍是使用的基于角色的受权方式。小程序

三、本文是 IS4 系列的铺垫文章。后端

 

1、JWT受权验证,咱们经历了哪些

看过我写的这个第一个系列《先后端分离》的小伙伴都知道,我用到了JWT来实现的权限验证,目前已经达到什么程度的验证了呢,这里我经历了三个步骤:微信小程序

这里强调下,若是你是第一次看这个文章,除非是有必定的基础,或者是一直跟着个人代码的,否则的话,会有点儿懵,若是不知足上边两个条件,请先看我以前的两篇文章,基础:

一、五 || Swagger的使用 3.3 JWT权限验证【修改】

二、36 ║解决JWT权限验证过时问题

 

一、直接在 Api 接口地址上设计 Roles 信息

这个也是最简单,最粗暴的方法,直接这么配置

    /// <summary>
    /// Values控制器
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    [Authorize(Roles = "Admin,Client")]
    [Authorize(Roles = "Admin")]
    [Authorize(Roles = "Client")]
    [Authorize(Roles = "Other")]
    public class ValuesController : ControllerBase
    {


    }

虽然咱们把 用户信息 和 角色Rols信息 保存到了数据库,实现了动态化,可是具体受权的时候,仍是须要手动在API接口地址上写特定的Role权限,这样才能对其进行匹配和受权,若是真的有一个接口能够被多个角色访问,那就须要垒了不少了,不是很好。

 

二、对不一样模块的角色们 创建策略

鉴于上边的问题,我考虑着对不一样的角色创建不一样的策略,并在 Startup.cs 启动类中,配置服务:

services.AddAuthorization(options =>
{
    options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());

    options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());

    options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));

    options.AddPolicy("SystemOrAdminOrOther", policy => policy.RequireRole("Admin", "System", "Other"));

})

而后在咱们的接口api上,只须要写上策略的名称便可:

 

相信你们也都是这么作的,固然我以前也是这么写的。虽然咱们在启动类 Startup.cs 中,对咱们的Roles作了策略,看起来不用一一的配置 Roles 了,可是你们会发现,好像这个功能并无想象中那么美丽,由于最关键的问题,咱们没有解决,由于这样咱们仍是须要手动一个接口一个接口的写权限策略,不灵活!我也是想了好久,才想到了今天的这个办法(请耐心往下看)。

 

三、将接口地址和角色受权分离

固然上边的方法也能实现咱们的小须要,每一个接口一个个都写好便可,可是做为强迫症的我,总感受会有办法能够把 API 接口,和 Role 权限剥离开,也能像用户和 Role那样,保存到数据库,实现动态分配,就这样我研究了微软的官方文档,偶然发现了微软官方文档的《Policy-based authorization》基于策略的受权,正好也找到了博客园一个大佬写的文章,我就使用了,这里注明下:借稿做者:asp.net core 2.0 web api基于JWT自定义策略受权》。

而后我在他的基础上,配合着我们的项目,作了调整,通过测试,完美的解决了我们的问题,能够动态的数据库进行配置,那具体是怎么实现的呢,请往下看。

 

2、接口地址和角色保存到数据库

数据库设计很差,你们看我写的思路便可,本身能够作扩展和优化,但愿仍是本身动手。

 既然要实现动态绑定,咱们就须要把接口地址信息、角色信息保存到数据库,那表结构是怎样的呢,其实目前个人数据库结构已经能够知足了要求了,只不过须要稍微调整下,由于以前我是用EF来设计的,这里用SqlSugar会出现一个问题,因此须要在 Blog.Core.Model 层引用 sqlSugarCore 的 Nuget 包,而后把实体 RoleModulePermission.cs 中的三个参数作下忽略处理。

一、实体模型设计

首先是接口和角色的关联表的实体模型:(真是Blog.Core项目中,可能会有些许变更,这里只是做为说明,若是想看真是的代码,请下载最新项目代码

namespace Blog.Core.Model.Models
{
    /// <summary>
    /// 接口、角色关联表(之后能够把按钮设计进来)
    /// </summary>
    public class RoleModulePermission
    {
        public int Id { get; set; }
        /// <summary>
        /// 角色ID
        /// </summary>
        public int RoleId { get; set; }
        /// <summary>
        /// 菜单ID,这里就是api地址的信息
        /// </summary>
        public int ModuleId { get; set; }
        /// <summary>
        /// 按钮ID
        /// </summary>
        public int? PermissionId { get; set; }
        /// <summary>
        /// 建立时间
        /// </summary>
        public DateTime? CreateTime { get; set; }
        /// <summary>
        ///获取或设置是否禁用,逻辑上的删除,非物理删除
        /// </summary>
        public bool? IsDeleted { get; set; }
        
        // 等等,还有其余属性,其余的能够参考Code,或者自定义...

        // 请注意,下边三个实体参数,只是作传参做用,因此忽略下,否则会认为缺乏字段
        [SugarColumn(IsIgnore = true)]
        public virtual Role Role { get; set; }
        [SugarColumn(IsIgnore = true)]
        public virtual Module Module { get; set; }
        [SugarColumn(IsIgnore = true)]
        public virtual Permission Permission { get; set; }
    }
}

 

而后就是API接口信息保存的实体模型:

namespace Blog.Core.Model.Models
{
    /// <summary>
    /// 接口API地址信息表
    /// </summary>
    public class Module
    {
        public int Id { get; set; }
        /// <summary>
        /// 父ID
        /// </summary>
        public int? ParentId { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// API连接地址
        /// </summary>
        public string LinkUrl { get; set; }
        /// <summary>
        /// 控制器名称
        /// </summary>
        public string Controller { get; set; }
        /// <summary>
        /// Action名称
        /// </summary>
        public string Action { get; set; }
        /// <summary>
        /// 图标
        /// </summary>
        public string Icon { get; set; }
        /// <summary>
        /// 菜单编号
        /// </summary>
        public string Code { get; set; }
        /// <summary>
        /// 排序
        /// </summary>
        public int OrderSort { get; set; }
        /// <summary>
        /// /描述
        /// </summary>
        public string Description { get; set; }
        /// <summary>
        /// 是否激活
        /// </summary>
        public bool Enabled { get; set; }

        // 等等其余属性,具体的能够看个人Code,或者本身自定义...
    }
}

 

总体数据库UML图以下(忽略箭头,没意义):(@铁梧桐 感谢提供,工具 PowerDesigner) 

 

二、Service 应用服务接口设计

这个很简单,CURD中,我只是简单写了一个查询所有关系的接口,其余的都很简单,相信本身也能搞定,IRepository.cs 、Repository.cs 和 IServices.cs 这三个我就很少写了,简单看下 Services.cs 的一个查询所有角色接口关系的方法:

namespace Blog.Core.Services
{
    /// <summary>
    /// RoleModulePermissionServices 应用服务
    /// </summary>    
    public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
    {

        IRoleModulePermissionRepository dal;
        IModuleRepository moduleRepository;
        IRoleRepository roleRepository;

        // 将多个仓储接口注入
        public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
        {
            this.dal = dal;
            this.moduleRepository = moduleRepository;
            this.roleRepository = roleRepository;
            base.baseDal = dal;
        }

        /// <summary>
        /// 获取所有 角色接口(按钮)关系数据 注意我使用我们以前的AOP缓存,很好的应用上了
        /// </summary>
        /// <returns></returns>
        [Caching(AbsoluteExpiration = 10)]
        public async Task<List<RoleModulePermission>> GetRoleModule()
        {
            var roleModulePermissions = await dal.Query(a => a.IsDeleted == false);
            if (roleModulePermissions.Count > 0)
            {
                foreach (var item in roleModulePermissions)
                {
                    item.Role = await roleRepository.QueryByID(item.RoleId);
                    item.Module = await moduleRepository.QueryByID(item.ModuleId);
                }

            }
            return roleModulePermissions;
        }

    }
}

 

我本身简单的设计了下数据,

表结构与数据,都已经经过 Codefirst+DataSeed 方式,支持MSSql、Oracle、Mysql、Sqlite等多种数据库,具体的查看个人 Github 的 README.md

 

 

这里设计使用外键,多对多的形式,能够很好的实现扩展,好比接口地址API变了,可是咱们使用的是id,能够很灵活的适应改变。

 

 

3、基于策略受权的自定义验证——核心

以前我们也使用过中间件 JwtTokenAuth 来进行受权验证,后来由于过时时间的问题,而后使用的官方的中间件app.UseAuthentication() ,今天我们就写一个3.0版本的验证方法,基于AuthorizationHandler 的权限受权处理器,具体的请往下看,若是看不懂,能够直接 pull 下个人 Github 代码便可。

一共是四个类:

 

一、JwtToken 生成令牌

这个很简单,就是咱们以前的 Token 字符串生成类,这里不过多作解释,只是要注意一下下边红色的参数 PermissionRequirement ,数据是从Startup.cs 中注入的,下边会说到。

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// JWTToken生成类
    /// </summary>
    public class JwtToken
    {
        /// <summary>
        /// 获取基于JWT的Token
        /// </summary>
        /// <param name="claims">须要在登录的时候配置</param>
        /// <param name="permissionRequirement">在startup中定义的参数</param>
        /// <returns></returns>
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
        {
            var now = DateTime.Now;
            // 实例化JwtSecurityToken
            var jwt = new JwtSecurityToken(
                issuer: permissionRequirement.Issuer,
                audience: permissionRequirement.Audience,
                claims: claims,
                notBefore: now,
                expires: now.Add(permissionRequirement.Expiration),
                signingCredentials: permissionRequirement.SigningCredentials
            );
            // 生成 Token
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            //打包返回前台
            var responseJson = new
            {
                success = true,
                token = encodedJwt,
                expires_in = permissionRequirement.Expiration.TotalSeconds,
                token_type = "Bearer"
            };
            return responseJson;
        }
    }
}

 

二、PermissionItem 凭据实体

说白了,这个就是用来存放咱们用户登陆成果后,在httptext中存放的角色信息的,是下边 必要参数类 PermissionRequirement 的一个属性,很简单,不细说:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 用户或角色或其余凭据实体
    /// </summary>
    public class PermissionItem
    {
        /// <summary>
        /// 用户或角色或其余凭据名称
        /// </summary>
        public virtual string Role { get; set; }
        /// <summary>
        /// 请求Url
        /// </summary>
        public virtual string Url { get; set; }
    }
}

 

三、PermissionRequirement 令牌必要参数类

这里边存放的都是 Jwt Token 的所有信息,注意它继承了 IAuthorizationRequirement,由于咱们要设计自定义受权验证处理器,因此必须继承验证要求接口,才能设计咱们本身的参数:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 必要参数类,
    /// 继承 IAuthorizationRequirement,用于设计自定义权限处理器PermissionHandler
    /// 由于AuthorizationHandler 中的泛型参数 TRequirement 必须继承 IAuthorizationRequirement
    /// </summary>
    public class PermissionRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 用户权限集合
        /// </summary>
        public List<PermissionItem> Permissions { get; 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; }
        /// <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>
        /// <param name="expiration">过时时间</param>
        public PermissionRequirement(string deniedAction, List<PermissionItem> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration)
        {
            ClaimType = claimType;
            DeniedAction = deniedAction;
            Permissions = permissions;
            Issuer = issuer;
            Audience = audience;
            Expiration = expiration;
            SigningCredentials = signingCredentials;
        }
    }
}

 

四、PermissionHandler 自定义受权处理器,核心!

 咱们先看代码:

namespace Blog.Core.AuthHelper
{
    /// <summary>
    /// 权限受权处理器 继承AuthorizationHandler ,而且须要一个权限必要参数 /// </summary>
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        /// <summary>
        /// 验证方案提供对象
        /// </summary>
        public IAuthenticationSchemeProvider Schemes { get; set; }

        /// <summary>
        /// services 层注入
        /// </summary>
        public IRoleModulePermissionServices _roleModulePermissionServices { get; set; }

        /// <summary>
        /// 构造函数注入
        /// </summary>
        /// <param name="schemes"></param>
        /// <param name="roleModulePermissionServices"></param>
        public PermissionHandler(IAuthenticationSchemeProvider schemes, IRoleModulePermissionServices roleModulePermissionServices)
        {
            Schemes = schemes;
            _roleModulePermissionServices = roleModulePermissionServices;
        }

        // 重写异步处理程序
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            // 将最新的角色和接口列表更新,
// 注意这里我用到了AOP缓存,只是减小与数据库的访问次数,而又保证是最新的数据
var data = await _roleModulePermissionServices.GetRoleModule(); var list = (from item in data where item.IsDeleted == false orderby item.Id select new PermissionItem { Url = item.Module?.LinkUrl, Role = item.Role?.Name, }).ToList(); requirement.Permissions = list; //从AuthorizationHandlerContext转成HttpContext,以便取出表头信息 var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //请求Url var questUrl = httpContext.Request.Path.Value.ToLower(); //判断请求是否中止 var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) { context.Fail(); return; } } //判断请求是否拥有凭据,即有没有登陆 var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); if (defaultAuthenticate != null) { var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name); //result?.Principal不为空即登陆成功 if (result?.Principal != null) { httpContext.User = result.Principal; //权限中是否存在请求的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.Role) && w.Url.ToLower() == questUrl).Count() <= 0) { context.Fail(); return; // 能够在这里设置跳转页面,不过仍是会访问当前接口地址的 httpContext.Response.Redirect(requirement.DeniedAction); } } else { context.Fail(); 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.Now) { context.Succeed(requirement); } else { context.Fail(); return; } 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); } } }

 

基本的解释上边已经写了,应该能看懂,这里只有一点,就是咱们自定义的这个处理器,是继承了AuthorizationHandler ,并且它还须要一个泛型类,而且该泛型类必须继承IAuthorizationRequirement 这个受权要求的接口,这样咱们就能够很方便的把咱们的自定义的权限参数传入受权处理器中。

 

好啦,到了这里,咱们已经设计好了处理器,那如何配置在启动服务中呢,请继续看。

 

4、配置受权服务与使用

 这里主要是在咱们的启动类 Startup.cs 中的服务配置,其实和以前的差很少,只是作了简单的封装,你们必定都能看的懂:

一、将JWT密钥等信息封装到配置文件

在接口层的 appsettings.json 文件中,配置咱们的jwt令牌信息:

"Audience": {
    "Secret": "sdfsdfsrty45634kkhllghtdgdfss345t678fs",
    "Issuer": "Blog.Core",
    "Audience": "wr"
  }

 

二、修改JWT服务注册方法

在启动类 Startup.cs 中的服务方法ConfigureServices 中,修改咱们的JWT Token 服务注册方法:

      #region JWT Token Service
            //读取配置文件
            var audienceConfig = Configuration.GetSection("Audience");
            var symmetricKeyAsBase64 = audienceConfig["Secret"];
            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
            var signingKey = new SymmetricSecurityKey(keyByteArray);

            // 令牌验证参数,以前咱们都是写在AddJwtBearer里的,这里提出来了
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,//验证发行人的签名密钥
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,//验证发行人
                ValidIssuer = audienceConfig["Issuer"],//发行人
                ValidateAudience = true,//验证订阅人
                ValidAudience = audienceConfig["Audience"],//订阅人
                ValidateLifetime = true,//验证生命周期
                ClockSkew = TimeSpan.Zero,//这个是定义的过时的缓存时间
                RequireExpirationTime = true,//是否要求过时

            };
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

            // 注意使用RESTful风格的接口会更好,由于只须要写一个Url便可,好比:/api/values 表明了Get Post Put Delete等多个。
            // 若是想写死,能够直接在这里写。
            //var permission = new List<PermissionItem> {
            //                  new PermissionItem {  Url="/api/values", Role="Admin"},
            //                  new PermissionItem {  Url="/api/values", Role="System"},
            //                  new PermissionItem {  Url="/api/claims", Role="Admin"},
            //              };

            // 若是要数据库动态绑定,这里先留个空,后边处理器里动态赋值
            var permission = new List<PermissionItem>();

            // 角色与接口的权限要求参数
            var permissionRequirement = new PermissionRequirement(
                "/api/denied",// 拒绝受权的跳转地址(目前无用)
                permission,//这里还记得么,就是咱们上边说到的角色地址信息凭据实体类 Permission
                ClaimTypes.Role,//基于角色的受权
                audienceConfig["Issuer"],//发行人
                audienceConfig["Audience"],//订阅人
                signingCredentials,//签名凭据
                expiration: TimeSpan.FromSeconds(60*2)//接口的过时时间,注意这里没有了缓冲时间,你也能够自定义,在上边的TokenValidationParameters的 ClockSkew
                );

            // ① 核心之一,配置受权服务,也就是具体的规则,已经对应的权限策略,好比公司不一样权限的门禁卡
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Client", 
                    policy => policy.RequireRole("Client").Build());
                options.AddPolicy("Admin", 
                    policy => policy.RequireRole("Admin").Build());
                options.AddPolicy("SystemOrAdmin", 
                    policy => policy.RequireRole("Admin", "System"));

                // 自定义基于策略的受权权限
                options.AddPolicy("Permission",
                         policy => policy.Requirements.Add(permissionRequirement));
            })
 // ② 核心之二,必须要配置认证服务,这里是jwtBearer默认认证,好比光有卡没用,得能识别他们
            .AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
 // ③ 核心之三,针对JWT的配置,好比门禁是如何识别的,是放射卡,仍是磁卡
            .AddJwtBearer(o =>
            {
                o.TokenValidationParameters = tokenValidationParameters;
            });


            // 依赖注入,将自定义的受权处理器 匹配给官方受权处理器接口,这样当系统处理受权的时候,就会直接访问咱们自定义的受权处理器了。
            services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
       // 将受权必要类注入生命周期内 services.AddSingleton(permissionRequirement);
#endregion

 注意必定要配置这三个核心(.AddAuthorization、.AddAuthentication、.AddJwtBearer),不然会报错:

三、在登陆接口中,赋值过时时间等信息 

 虽然咱们在 startup 中也设置了过时时间,可是咱们还须要在每个 token 的声明列表中(claims)中,配置过时时间,只不过两个时间同样罢了。

 

四、在接口中很方便调用

这样定义好之后,咱们只须要很方便的在每个controller上边写上 [Authorize("Permission")],这个验证特性便可,这个名字就是咱们的策略名,咱们就不用再想哪个接口对应哪些Roles了,是否是更方便了!固然若是不写这个特性的话,不会被限制,好比那些前台的页面接口,就不须要被限制。

 

 

五、使用效果展现

我们看看平时会遇到的4种状况。

注意:下边的演示,是用的  public async Task<object> GetJWTToken3(string name, string pass)  这个新接口获取的Token

 

你也能够直接使用个人在线地址 http://123.206.33.109:8081/swagger/index.html 来操做,具体的步骤见下面的这三个状况。

 

接口没有配置权限

这种状况,不管是数据库是否配置,都会很正常的经过HTTP请求,从而获取到咱们的数据,就好比登陆页:

 

接口设置了权限,可是数据库没有配置

我们以 ValuesController 为例子

 

 如今咱们把API接口是 /api/values 的接口和角色关联的表给逻辑删除了,那这个时候,也就表明了,当前接口虽然设置了权限,可是在数据库里并无配置它与Role的关系:

 

那若是咱们访问的话会是怎样:

 

首先,咱们看到在获取到的四个角色接口信息中,已经没有了api/values 的相关信息,而后咱们去访问该接口,就看到了报错403,固然你也能够自定义错误,就是在 PermissionHandler.cs 自定义权限受权处理程序里,能够本身扩展。

 

接口设置了权限,而且数据库也配置了

仍是使用我们的 ValueController.cs ,这时候我们把刚刚逻辑删除的改为False:

 

 

而后看看咱们的执行过程:

 

 

发现咱们已经很成功的对接口进行了权限控制了,你能够在后台作成界面的形式,对其进行配置等,固然很丰富的了。这里要说一下,若是你用的是RESTful风格的接口,配置 api/values 会把CURD四个权限所有赋过去,若是你想对某一个角色只有Read和Create(读取和添加)的权限的话,你能够这么操做:

一、不用RESTful风格,直接每个接口都配置进去,好比这样,api/values/Read、api/values/Create 等;

二、继续使用RESTful风格接口,可是须要在(角色和API地址的)数据库表中,再增长一个 ActionName 这样相似的字段,对接口进行区分限制就行,具体的,我会在下一个系列说到;

 

最后通过了两分钟,令牌过时:

 

好啦,这些简单的受权功能已经够我们使用了,还能在数据库里动态配置,不是么?

 

5、思考

到这里,我们的这个项目已经彻底能实现权限的动态分配了,固然这里是没有后台界面的,你能够本身创建一个MVC项目来实验,也能够创建一个Vue管理后台来分配,都是很简单的,我我的感受已经很完美了,我们的项目基本也成型了。

可是这些都是我们本身造的轮子,那若是咱们用一直很高调的 IdentityServer4 这个已经成熟的轮子来实现接口级别的动态受权是怎么作呢?

请看个人下一个系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提早祝你们圣诞节快乐!

 

一、情景补充

有的小伙伴在研究或者使用这个方法的时候,出现了疑惑,主要是两个问题:

一、我若是后台修改权限了,想马上或者 关闭浏览器下次打开的时候更新权限咋办?

二、若是我Token的过时时间比较短,好比一天,那如何实现滑动更新,就是不会正在使用的时候,忽然去登陆页?

我也想了想,大概有如下本身的想法,你们能够参考一下,欢迎提出批评:

一、若是后台管理员修改了某一我的的权限,我会把每个Token放到Redis缓存里,而后主要是 Token 的值,还有过时时间,权限等,若是管理员修改了权限(这个时候Token就不能使用了,由于这个Token仍是以前的Roles权限),而后就会更新了数据库的Role,还会把Redis里的该Token信息给Delete掉,这样用户再访问下一个页面的时候,咱们先校验Redis缓存里是否有这个 Token 数据,若是有,还继续往下走,若是没有了,就返回401让用户从新登陆。可使用一个中间件来处理当前Token是否有效。

 

二、上边写到了在net core api里增长一个中间件来判断Token是否有效,那若是无效了或者是被管理员修改了权限,致使 Token 被禁掉之后,又不想让用户从新登陆怎么办呢,我就想的是在 Http.js 封装请求方法中,写一个,每次用户访问的以前,都判断一下当前 Token 是否有效的JS方法,若是有效则继续调用下一个接口,若是无效,这个时候就能够在后台从新生成一个 Token 并返回到前台,保存到localstroage里,继续用新的 Token 调用下一个接口。

 

三、用上边的方法,你会感受这样每次都会多一次调用,会占资源,你能够天天执行一次,或者就是每次登陆的成功后,不只把 Token 存在本地,把过时时间也存下来,这样每次请求前能够判断是否过时,若是过时了呢,就先调用从新获取Token 的接口方法,而后再往下走。

 可能你会感受很麻烦,很荒唐,不过微信小程序就是这么处理的,不信你能够去研究下。

 

6、Github & Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

 

-- END