从壹开始前后端分离[.NetCore] 37 ║JWT如何实现权限与接口的动态分配——复杂策略授权

缘起 本文相关视频讲解:https: www bilibili com video BV1sE411y7ot?p=4 本文已经有了对应的管理后台,地址:ht

缘起

本文相关视频讲解:https://www.bilibili.com/video/BV1sE411y7ot?p=4

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

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

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

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

好啦,废话不多说,因为今天是不定期更新系列,所以会之间进入主题,不会有概念性的讲解,马上开始今天的内容啦!主要是以下几个方面:

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

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

2、接口中,之前的授权方法依然保留,在 BlogController.cs 中还是使用的基于角色的授权方式。

3、本文是 IS4 系列的铺垫文章。

 

一、JWT授权验证,我们经历了哪些

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

这里强调下,如果你是第一次看这个文章,除非是有一定的基础,或者是一直跟着我的代码的,不然的话,会有点儿懵,如果不满足上边两个条件,请先看我之前的两篇文章,基础:

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

2、36 ║解决JWT权限验证过期问题

 

1、直接在 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权限,这样才能对其进行匹配和授权,如果真的有一个接口可以被多个角色访问,那就需要垒了很多了,不是很好。

 

2、对不同模块的角色们 建立策略

鉴于上边的问题,我考虑着对不同的角色建立不同的策略,并在 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 了,但是大家会发现,好像这个功能并没有想象中那么美丽,因为最关键的问题,我们没有解决,因为这样我们还是需要手动一个接口一个接口的写权限策略,不灵活!我也是想了很久,才想到了今天的这个办法(请耐心往下看)。

 

3、将接口地址和角色授权分离

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

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

 

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

数据库设计不好,大家看我写的思路即可,自己可以做扩展和优化,希望还是自己动手。

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

1、实体模型设计

首先是接口和角色的关联表的实体模型:(真是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) 

 

2、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,可以很灵活的适应改变。

 

 

三、基于策略授权的自定义验证——核心

之前咱们也使用过中间件 JwtTokenAuth 来进行授权验证,后来因为过期时间的问题,然后使用的官方的中间件app.UseAuthentication() ,今天咱们就写一个3.0版本的验证方法,基于AuthorizationHandler 的权限授权处理器,具体的请往下看,如果看不懂,可以直接 pull 下我的 Github 代码即可。

一共是四个类:

 

1、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;
        }
    }
}

 

2、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; }
    }
}

 

3、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;
        }
    }
}

 

4、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 这个授权要求的接口,这样我们就可以很方便的把我们的自定义的权限参数传入授权处理器中。

 

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

 

四、配置授权服务与使用

 这里主要是在我们的启动类 Startup.cs 中的服务配置,其实和之前的差不多,只是做了简单的封装,大家一定都能看的懂:

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

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

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

 

2、修改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),否则会报错:

3、在登录接口中,赋值过期时间等信息 

 虽然我们在 startup 中也设置了过期时间,但是我们还需要在每一个 token 的声明列表中(claims)中,配置过期时间,只不过两个时间一样罢了。

 

4、在接口中很方便调用

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

 

 

5、使用效果展示

咱们看看平时会遇到的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(读取和添加)的权限的话,你可以这么操作:

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

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

 

最后经过了两分钟,令牌过期:

 

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

 

五、思考

到这里,咱们的这个项目已经完全能实现权限的动态分配了,当然这里是没有后台界面的,你可以自己建立一个MVC项目来实验,也可以建立一个Vue管理后台来分配,都是很简单的,我个人感觉已经很完美了,咱们的项目基本也成型了。

但是这些都是咱们自己造的轮子,那如果我们用一直很高调的 IdentityServer4 这个已经成熟的轮子来实现接口级别的动态授权是怎么做呢?

请看我的下一个系列吧(.NetCore API + IS4+EFCore+VueAdmin)~~~ 提前祝大家圣诞节快乐!

 

1、情景补充

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

1、我如果后台修改权限了,想立刻或者 关闭浏览器下次打开的时候更新权限咋办?

2、如果我Token的过期时间比较短,比如一天,那如何实现滑动更新,就是不会正在使用的时候,突然去登录页?

我也想了想,大概有以下自己的想法,大家可以参考一下,欢迎提出批评:

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

 

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

 

3、用上边的方法,你会感觉这样每次都会多一次调用,会占资源,你可以每天执行一次,或者就是每次登录的成功后,不仅把 Token 存在本地,把过期时间也存下来,这样每次请求前可以判断是否过期,如果过期了呢,就先调用重新获取Token 的接口方法,然后再往下走。

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

 

六、Github & Gitee

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

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

 

-- END

您可能有感兴趣的文章
【实践篇】领域驱动设计:DDD工程参考架构

IDDD 如何实现领域驱动设计-理解限界上下文

DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(1)

领域驱动设计(DDD):三层架构到DDD架构演化

【读书笔记】【设计】如何实现领域驱动设计(DDD)笔记