ASP.NET CORE 第十篇 JWT完美实现权限与接口的动态分配

原文作者:老张的哲学

一、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上,只需要写上策略的名称即可:

image

相信大家也都是这么做的,当然我之前也是这么写的。虽然我们在启动类 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接口信息保存的实体模型:

using System;
using System.Collections.Generic;
using System.Text;

namespace Blog.Core.Model
{
    /// <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)

image

2、Service 应用服务接口设计
这个很简单,CURD中,我只是简单写了一个查询全部关系的接口,其他的都很简单,相信自己也能搞定,IRepository.cs 、Repository.cs 和 IServices.cs 这三个我就不多写了,简单看下 Services.cs 的一个查询全部角色接口关系的方法:

using Blog.Core.Common.Attribue;
using Blog.Core.IRepository;
using Blog.Core.IServices;
using Blog.Core.Model.Models;
using Blog.Core.Services.Base;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Blog.Core.Services
{
    /// <summary>
    /// RoleModulePermissionServices 应用服务
    /// </summary>
    public class RoleModulePermissionServices : BaseServices<RoleModulePermission>, IRoleModulePermissionServices
    {
        readonly IRoleModulePermissionRepository dal;
        readonly IModuleRepository moduleRepository;
        readonly IRoleRepository roleRepository;

        /// <summary>
        /// // 将多个仓储接口注入
        /// </summary>
        public RoleModulePermissionServices(IRoleModulePermissionRepository dal, IModuleRepository moduleRepository, IRoleRepository roleRepository)
        {
            this.dal = dal;
            this.moduleRepository = moduleRepository;
            this.roleRepository = roleRepository;
            this.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;
        }

        public async Task<List<TestMuchTableResult>> QueryMuchTable()
        {
            return await dal.QueryMuchTable();
        }

        public async Task<List<RoleModulePermission>> TestModelWithChildren()
        {
            return await dal.WithChildrenModel();
        }
    }
}

我自己简单的设计了下数据,如果有想我的数据的,请留言,我把这个Sql数据文件放到 Github :https://github.com/anjoy8/Blog.Core/blob/master/Blog.Core/wwwroot/权限.sql

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

image

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

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

一共是四个类:

image

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

image

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

四、配置授权服务与使用

这里主要是在我们的启动类 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),否则会报错:

image

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

image

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

4、在接口中很方便调用

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

image

5、使用效果展示

咱们看看平时会遇到的4种情况。

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

image

你也可以直接使用我的在线地址 http://123.206.33.109:8081/swagger/index.html 来操作,具体的步骤见下面的这三个情况。

接口没有配置权限

这种情况,无论是数据库是否配置,都会很正常的通过HTTP请求,从而获取到我们的数据,就比如登录页:

image

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

咱们以 ValuesController 为例子


image

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

image

那如果我们访问的话会是怎样:

image

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

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

还是使用咱们的 ValueController.cs ,这时候咱们把刚刚逻辑删除的改成False:

image

然后看看我们的执行过程:

image

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

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

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

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

image

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

五、思考

到这里,咱们的这个项目已经完全能实现权限的动态分配了,当然这里是没有后台界面的,你可以自己建立一个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 的接口方法,然后再往下走。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容