1, 前言
但凡业务系统, 授权是绕不开的一环. 见过太多只在前端做菜单及按钮显隐控制, 但后端裸奔的, 觉着前端看不到, 系统就安全, 掩耳盗铃也好, 自欺欺人也罢, 这里不做评论. 在. NET CORE 中, 也见过不少用操作过滤器来实现业务用例权限控制的, 至少算是对后端做了权限控制.
但我们知道, 操作过滤器, 已经算是过滤器管道中最靠后的, 基本上紧挨着我们控制器方法执行那里了, 本身, 操作过滤器也不是做权限控制的地方, 虽然本身它能达到权限控制效果. 为什么这么说, 试想下, 在过滤器管道之前, 还有中间件处理管道, 即便是过滤器管道执行环节, 操作过滤器也是最靠后的, 它往前还有授权过滤器, 资源过滤器等, 假如我在资源过滤器中缓存了请求结果, 那权限控制基本上就废了.
说这么多, 只想表达一点, 合适的地方, 合适的东西, 干合适的事儿. 在. NET CORE 中, 官方推荐用策略去实现授权. 策略授权, 是在授权中间件环节执行, 当然能解决上述执行流程先后顺序的问题. 但如果要直接应用于我们业务系统中的权限控制, 恐怕远远不够, 因为你不可能为每个 API 用例创建一个角色或策略, 更主要的, 权限控制还要动态授予或回收的, 不做扩展直接照搬, 你是很难搞的. 接下来, 我们就来看看, 如何基于 core 的授权机制, 去实现我们传统的用户, 角色, 权限, 及权限的动态授予与回收控制.
2, 实现
我们先看看, 菜单表概览:
查询中 IsMenu 代表是侧边栏菜单还是功能按钮, 这里我把按钮级别的给筛选出来了, 每个按钮菜单都代表一个业务用例, 也对应我们一个控制器方法. Code 是唯一的, 待会儿权限控制标识, 会采用这个字段. 当然你用主键 ID 也可以, 但比较难记. 实际运维中, 会把这些菜单按照业务分配给指定角色, 再把指定角色分配给系统用户.
接下来, 定义一个 Requirement, 以权限码作为主校验对象:
- public class PermissionRequirement : IAuthorizationRequirement
- {
- public PermissionRequirement(params string[] codes)
- {
- this.Codes = codes;
- }
- public string[] Codes { get; private set; }
- }
有 Requirement, 自然有 RequirementHandler:
- public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
- {
- private readonly IMenuService _menuService;
- public PermissionHandler(IMenuService menuService)
- {
- _menuService = menuService;
- }
- protected override async Task HandleRequirementAsync(
- AuthorizationHandlerContext context,
- PermissionRequirement requirement)
- {
- var roles = context.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role).Value;
- if (!string.IsNullOrWhiteSpace(roles))
- {
- var roleIds = roles.Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(x => long.Parse(x));
- if (roleIds.Contains(1))
- {
- context.Succeed(requirement);
- return;
- }
- var menus = await _menuService.GetMenusByRoleIds(roleIds.ToArray());
- if (menus.Any())
- {
- var codes = menus.Select(x => x.Code.ToUpper());
- if (requirement.Codes.Any(x => codes.Contains(x.ToUpper())))
- {
- context.Succeed(requirement);
- }
- }
- }
- }
- }
逻辑比较常规, 根据当前用户角色, 获取其所有菜单权限, 然后与 Requirement 中声明要求的菜单权限做对比, 如果含有, 则放行. 到这儿, 大家应该都能看懂, 典型的. NET CORE 权限控制组件.
接下来, 定义一个授权过滤器特性:
- public class PermissionFilter : Attribute, IAsyncAuthorizationFilter
- {
- private readonly IAuthorizationService _authorizationService;
- private readonly PermissionRequirement _requirement;
- public PermissionFilter(IAuthorizationService authorizationService, PermissionRequirement requirement)
- {
- _authorizationService = authorizationService;
- _requirement = requirement;
- }
- public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
- {
- var result = await _authorizationService.AuthorizeAsync(context.HttpContext.User, null, _requirement);
- if (!result.Succeeded)
- {
- context.Result = new JsonResult("您没有此操作权限")
- {
- StatusCode = (int)HttpStatusCode.Forbidden
- };
- }
- }
- }
从这儿开始, 我估计有人就要看不懂了, 下边解释下. 首先, PermissionFilter 是个授权过滤器, 它实现了 IAsyncAuthorizationFilter, 所以会作为过滤器管道第一道去执行. 构造函数中有 2 个参数, IAuthorizationService 直接注入, PermissionRequirement 则通过特性标记外部设置. 在 OnAuthorizationAsync 重写方法中, 调用 IAuthorizationService.AuthorizeAsync 方法去做具体授权校验工作, 经此桥接, 授权流程就转到我们最开始定义的 PermissionHandler 去了. 本身 core 源码中, IAuthorizationService 是在授权中间件中使用到的, 这里我借用了. 注意, 一旦过滤器注入了服务, 那此过滤器便不再能够直接以打标记的形式贴在控制器或方法上了, 那种形式必须所有参数都直接指定. core 中给出的方案, 是 TypeFilter, 涉及到服务注入的时候.
那接下来, 就是另一个重要对象了:
- /// <summary>
- /// 权限特性
- /// </summary>
- public class PermissionAttribute : TypeFilterAttribute
- {
- public PermissionAttribute(params string[] codes)
- : base(typeof(PermissionFilter))
- {
- Arguments = new[] { new PermissionRequirement(codes) };
- }
- }
至此, 各组件定义完毕, 那我们使用就类似下边这样:
3, 效果演示
guokun 用户角色:
网站管理员角色对应权限:
可以看到, 是没有勾选删除部门的, 那我们用这个账户来删除下部门:
状态码 403, 并提示无权操作, 删除动作已经被拦截了. 那我们把删除权限赋予网站管理员:
接下来再来删除:
可以看到, 已经删除部门成功.
3, 总结
以上便是本项目权限控制的实现. 认证 & 授权这块儿, 如果要做好, 还是得把 core 的整套机制弄清楚, 最好能把源码过一遍, 不然根本搞不清楚需要怎么扩展, 每个扩展点在什么时机触发及生效.
来源: https://www.cnblogs.com/guokun/p/12492155.html