前言

  上个月,我写了两篇微服务的文章:《.Net微服务实战之技术架构分层篇》与《.Net微服务实战之技术选型篇》,微服务系列原有三篇,当我憋第三篇的内容时候一直没有灵感,因此先打算放一放。

  本篇文章与源码原本打算实在去年的时候完成并发布的,然而我一直忙于公司项目的微服务的实施,所以该篇文章一拖再拖。如今我花了点时间整理了下代码,并以此篇文章描述整个实现思路,并开放了源码给予需要的人一些参考。

  源码:https://github.com/SkyChenSky/Sikiro.RBAC

RBAC

  Role-Based Access Contro翻译成中文就是基于角色的访问控制,文章以下我都用他的简称RBAC来描述。

  现信息系统的权限控制大多数采取RBAC的思想进行实现,其本质思想是对系统各种的操作权限不是直接授予具体的某个用户,而是在用户集合与权限集合之间建立一个角色,作为间接关联。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。

  通过以上的描述,我们可以分析出以下信息:

  •   用户与权限是通过角色间接关联的
  •   角色的本质就是权限组(权限集合)

  这样做的好处在于,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

  

功能分析

权限分类

从权限的作用可以分为三种,功能权限、访问权限、数据权限

  • 功能权限

    • 功能权限指系统用户允许在页面进行按钮操作的权限。如果有权限则功能按钮展示,否则隐藏。
  • 访问权限
    • 访问权限指系统用户通过点击按钮后进行地址的请求访问的权限(地址跳转与接口请求),如果无权限访问,则由页面提示无权限访问。
  • 数据权限
    • 数据权限指用户可访问系统的数据权限,不同的用户可以访问不同的数据粒度。

数据权限的实现可大可小,大可大到对条件进行动态配置,小可小到只针对某个维度进行硬编码。不纳入这次的讨论范围。

用例图

非功能性需求

  时效性,直接影响到安全性,既然是权限控制,那么理应一修改权限后就立刻生效。曾经有同行问过我,是不是每一个请求都得去查一次数据库是否满足权限,如果是,数据库压力岂不是很大?

  安全性,每一个页面跳转,每一个读写请求都的进行一次权限验证,不满足的权限的功能按钮就不需要渲染,避免样式display:none的情况。

  开发效率,权限控制理应是框架层面的,因此尽可能作为非业务的侵入性,让开发人员保持原有的数据善增改查与页面渲染。

技术选型

LayUI

  学习门槛极低,开箱即用。其外在极简,却又不失饱满的内在,体积轻盈,组件丰盈,从核心代码到 API 的每一处细节都经过精心雕琢,非常适合界面的快速开发,它更多是为服务端程序员量身定做,无需涉足各种前端工具的复杂配置,只需面对浏览器本身,让一切你所需要的元素与交互,从这里信手拈来。作为国人的开源项目,完整的接口文档与Demo示例让入门者非常友好的上手,开箱即用的Api让学习成本尽可能的低,其易用性成为快速开发框架的基础。

MongoDB

  主要两大优势,无模式与横向扩展。对于权限模块来说,无需SQL来写复杂查询和报表,也不需要使用到多表的强事务,上面提到的时效性的数据库压力问题也可以通过分片解决。无模式使得开发人员无需预定义存储结构,结合MongoDB官方提供的驱动可以做到快速的开发。

数据库设计

E-R图

  一个管理员可以拥有多个角色,因此管理员与角色是一对多的关联;角色作为权限组的存在,又可以选择多个功能权限值与菜单,所以角色与菜单、功能权限值也是一对多的关系。

类图

Deparment与Position属于非核心,可以按照自己的实际业务进行扩展。

功能权限值初始化

  随着业务发展,需求功能是千奇百怪的,根本无法抽象出来,那么功能按钮就要随着业务进行定义。在我的项目里使用了枚举值进行定义每个功能权限,通过自定义的PermissionAttribute与响应的action进行绑定,在系统启动时,通过反射把功能权限的枚举值与相应的controller、action映射到MenuAction表,枚举值对应code字段,controller与action拼接后对应url字段。

  已初始化到数据库的权限值可以到菜单页把相对应的菜单与权限通过用户界面关联起来。

权限值绑定action

         [HttpPost]
[Permission(PermCode.Administrator_Edit)]
public IActionResult Edit(EditModel edit)
{
//do something return Json(result);
}

初始化权限值

     /// <summary>
/// 功能权限
/// </summary>
public static class PermissionUtil
{
public static readonly Dictionary<string, IEnumerable<int>> PermissionUrls = new Dictionary<string, IEnumerable<int>>();
private static MongoRepository _mongoRepository; /// <summary>
/// 判断权限值是否被重复使用
/// </summary>
public static void ValidPermissions()
{
var codes = Enum.GetValues(typeof(PermCode)).Cast<int>();
var dic = new Dictionary<int, int>();
foreach (var code in codes)
{
if (!dic.ContainsKey(code))
dic.Add(code, );
else
throw new Exception($"权限值 {code} 被重复使用,请检查 PermCode 的定义");
}
} /// <summary>
/// 初始化添加预定义权限值
/// </summary>
/// <param name="app"></param>
public static void InitPermission(IApplicationBuilder app)
{
//验证权限值是否重复
ValidPermissions(); //反射被标记的Controller和Action
_mongoRepository = (MongoRepository)app.ApplicationServices.GetService(typeof(MongoRepository)); var permList = new List<MenuAction>();
var actions = typeof(PermissionUtil).Assembly.GetTypes()
.Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract)
.SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)); //遍历集合整理信息
foreach (var action in actions)
{
var permissionAttribute =
action.GetCustomAttributes(typeof(PermissionAttribute), false).ToList();
if (!permissionAttribute.Any())
continue; var codes = permissionAttribute.Select(a => ((PermissionAttribute)a).Code).ToArray();
var controllerName = action?.ReflectedType?.Name.Replace("Controller", "").ToLower();
var actionName = action.Name.ToLower(); foreach (var item in codes)
{
if (permList.Exists(c => c.Code == item))
{
var menuAction = permList.FirstOrDefault(a => a.Code == item);
menuAction?.Url.Add($"{controllerName}/{actionName}".ToLower());
}
else
{
var perm = new MenuAction
{
Id = item.ToString().EncodeMd5String().ToObjectId(),
CreateDateTime = DateTime.Now,
Url = new List<string> { $"{controllerName}/{actionName}".ToLower() },
Code = item,
Name = ((PermCode)item).GetDisplayName() ?? ((PermCode)item).ToString()
};
permList.Add(perm);
}
}
PermissionUrls.TryAdd($"{controllerName}/{actionName}".ToLower(), codes);
} //业务功能持久化
_mongoRepository.Delete<MenuAction>(a => true);
_mongoRepository.BatchAdd(permList);
} /// <summary>
/// 获取当前路径
/// </summary>
/// <param name="filterContext"></param>
/// <returns></returns>
public static string CurrentUrl(HttpContext filterContext)
{
var url = filterContext.Request.Path.ToString().ToLower().Trim('/');
return url;
}
}

关联菜单与功能权限

访问权限

  当所有权限关系关联上后,用户访问系统时,需要对其所有操作进行拦截与实时的权限判断,我们注册一个全局的GlobalAuthorizeAttribute,其主要拦截所有已经标识PermissionAttribute的action,查询该用户所关联所有角色的权限是否满足允许通过。

  我的实现有个细节,给判断用户IsSuper==true,也就是超级管理员,如果是超级管理员则绕过所有判断,可能有人会问为什么不在角色添加一个名叫超级管理员进行判断,因为名称是不可控的,在代码逻辑里并不知道用户起的所谓的超级管理员,就是我们需要绕过验证的超级管理员,假如他叫无敌管理员呢?

  /// <summary>
/// 全局的访问权限控制
/// </summary>
public class GlobalAuthorizeAttribute : System.Attribute, IAuthorizationFilter
{
#region 初始化
private string _currentUrl;
private string _unauthorizedMessage;
private readonly List<string> _noCheckPage = new List<string> { "home/index", "home/indexpage", "/" }; private readonly AdministratorService _administratorService;
private readonly MenuService _menuService; public GlobalAuthorizeAttribute(AdministratorService administratorService, MenuService menuService)
{
_administratorService = administratorService;
_menuService = menuService;
}
#endregion public void OnAuthorization(AuthorizationFilterContext context)
{
context.ThrowIfNull(); _currentUrl = PermissionUtil.CurrentUrl(context.HttpContext); //不需要验证登录的直接跳过
if (context.Filters.Count(a => a is AllowAnonymousFilter) > )
return; var user = GetCurrentUser(context);
if (user == null)
{
if (_noCheckPage.Contains(_currentUrl))
return; _unauthorizedMessage = "登录失效"; if (context.HttpContext.Request.IsAjax())
NoUserResult(context);
else
LogoutResult(context);
return;
} //超级管理员跳过
if (user.IsSuper)
return; //账号状态判断
var administrator = _administratorService.GetById(user.UserId);
if (administrator != null && administrator.Status != EAdministratorStatus.Normal)
{
if (_noCheckPage.Contains(_currentUrl))
return; _unauthorizedMessage = "亲~您的账号已被停用,如有需要请您联系系统管理员"; if (context.HttpContext.Request.IsAjax())
AjaxResult(context);
else
AuthResult(context, , GoErrorPage(true)); return;
} if (_noCheckPage.Contains(_currentUrl))
return; var userUrl = _administratorService.GetUserCanPassUrl(user.UserId); // 判断菜单访问权限与菜单访问权限
if (IsMenuPass(userUrl) && IsActionPass(userUrl))
return; if (context.HttpContext.Request.IsAjax())
AuthResult(context, , GetJsonResult());
else
AuthResult(context, , GoErrorPage());
}
}

功能权限

  在权限验证通过后,返回view之前,还是利用了Filter进行一个实时的权限查询,主要把该用户所拥有功能权限值查询出来通过ViewData["PermCodes"]传到页面,然后通过razor进行按钮的渲染判断。

  然而我在项目中封装了大部分常用的LayUI控件,主要利用.Net Core的TagHelper进行了封装,TagHelper内部与ViewData["PermCodes"]进行判断是否输出HTML。

全局功能权限值查询

 /// <summary>
/// 全局用户权限值查询
/// </summary>
public class GobalPermCodeAttribute : IActionFilter
{
private readonly AdministratorService _administratorService; public GobalPermCodeAttribute(AdministratorService administratorService)
{
_administratorService = administratorService;
} private static AdministratorData GetCurrentUser(HttpContext context)
{
return context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.UserData)?.Value.FromJson<AdministratorData>();
} public void OnActionExecuting(ActionExecutingContext context)
{
((Controller)context.Controller).ViewData["PermCodes"] = new List<int>(); if (context.HttpContext.Request.IsAjax())
return; var user = GetCurrentUser(context.HttpContext);
if (user == null)
return; if (user.IsSuper)
return; ((Controller)context.Controller).ViewData["PermCodes"] = _administratorService.GetActionCode(user.UserId).ToList();
} public void OnActionExecuted(ActionExecutedContext context)
{
}
}

LayUI Buttom的TagHelper封装

   [HtmlTargetElement("LayuiButton")]
public class LayuiButtonTag : TagHelper
{
#region 初始化
private const string PermCodeAttributeName = "PermCode";
private const string ClasstAttributeName = "class";
private const string LayEventAttributeName = "lay-event";
private const string LaySubmitAttributeName = "LaySubmit";
private const string LayIdAttributeName = "id";
private const string StyleAttributeName = "style"; [HtmlAttributeName(StyleAttributeName)]
public string Style { get; set; } [HtmlAttributeName(LayIdAttributeName)]
public string Id { get; set; } [HtmlAttributeName(LaySubmitAttributeName)]
public string LaySubmit { get; set; } [HtmlAttributeName(LayEventAttributeName)]
public string LayEvent { get; set; } [HtmlAttributeName(ClasstAttributeName)]
public string Class { get; set; } [HtmlAttributeName(PermCodeAttributeName)]
public int PermCode { get; set; } [HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; } #endregion
public override async void Process(TagHelperContext context, TagHelperOutput output)
{
context.ThrowIfNull();
output.ThrowIfNull(); var administrator = ViewContext.HttpContext.GetCurrentUser();
if (administrator == null)
return; var childContent = await output.GetChildContentAsync(); if (((List<int>)ViewContext.ViewData["PermCodes"]).Contains(PermCode) || administrator.IsSuper)
{
foreach (var item in context.AllAttributes)
{
output.Attributes.Add(item.Name, item.Value);
} output.TagName = "a";
output.TagMode = TagMode.StartTagAndEndTag;
output.Content.SetHtmlContent(childContent.GetContent());
}
else
{
output.TagName = "";
output.TagMode = TagMode.StartTagAndEndTag;
output.Content.SetHtmlContent("");
}
}
}

视图代码

结尾

  以上就是我本篇分享的内容,项目是以单体应用提供的,方案思路也适用于前后端分离。最后附上几个系统效果图

.Net Core实战之基于角色的访问控制的设计的更多相关文章

  1. RBAC基于角色的访问控制

    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成"用 ...

  2. RBAC(Role-Based Access Control,基于角色的访问控制)

    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成“用户-角色- ...

  3. YIi 权限管理和基于角色的访问控制

    验证和授权(Authentication and Authorization) 定义身份类 (Defining Identity Class) 登录和注销(Login and Logout) 访问控制 ...

  4. RBAC(Role-Based Access Control)基于角色的访问控制

    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成"用 ...

  5. 移动服务和 Azure Active Directory 中基于角色的访问控制

    编辑人员注释:本文章由 Matthew Henderson撰写 去年 11月,我们发布了 Azure Active Directory (AAD) 预览版作为移动服务身份提供程序.此举旨在为企业开 ...

  6. Azure ARM (16) 基于角色的访问控制 (Role Based Access Control, RBAC) - 使用默认的Role

    <Windows Azure Platform 系列文章目录> 今天上午刚刚和客户沟通过,趁热打铁写一篇Blog. 熟悉Microsoft Azure平台的读者都知道,在老的Classic ...

  7. ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

    一.前言 在非静态页面的项目开发中,必定会涉及到对于数据库的访问,最开始呢,我们使用 Ado.Net,通过编写 SQL 帮助类帮我们实现对于数据库的快速访问,后来,ORM(Object Relatio ...

  8. 基于角色的访问控制 (RBAC)权限管理

    RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联.简单地说,一个用户拥有若干角色,每一个角色拥有若干权限.这样,就构造成“用户-角色- ...

  9. 普通程序员看k8s基于角色的访问控制(RBAC)

    一.知识准备 ● 上一节描述了k8s的账户管理,本文描述基于角色的访问控制 ● 网上RBAC的文章非常多,具体概念大神们也解释得很详细,本文没有站在高屋建瓴的角度去描述RBAC,而是站在一个普通程序员 ...

随机推荐

  1. 【Python代码】TSNE高维数据降维可视化工具 + python实现

    目录 1.概述 1.1 什么是TSNE 1.2 TSNE原理 1.2.1入门的原理介绍 1.2.2进阶的原理介绍 1.2.2.1 高维距离表示 1.2.2.2 低维相似度表示 1.2.2.3 惩罚函数 ...

  2. 安装OPENCTI

    应业务需求,需要安装OPENCTI.很无奈的配了一下午. 首先是安装需求: 1. Ubuntu 2. Docker version 19.03.5 + docker-compose version 1 ...

  3. 测试工程中引入Masonry记录

    测试工程中需要引入Masonry,在进行添加新库时发现了几个问题,记录如下,方便有相同问题的朋友查找解决:   1,podfile中添加 pod ‘Masonry’ 后,pod install --v ...

  4. 2017-ACM南宁网络赛

    In this problem, we will define a graph called star graph, and the question is to find the minimum d ...

  5. 【python爬虫】scrapy入门5--xpath等后面接正则

    比如我们要调试某网页:https://g.widora.cn/ shell不依赖工程环境 scrapy shell https://g.widora.cn/ 类似页面F12,可用对象都列出来了,一般常 ...

  6. format函数格式化显示的方法

    数字 格式 输出 描述 3.1415926 {:.2f} 3.14 保留小数点后两位 3.1415926 {:+.2f} +3.14 带符号保留小数点后两位 -1 {:+.2f} -1.00 带符号保 ...

  7. 谈谈MySQL 索引

    1.索引是什么 索引(Index)是帮助MySQL高效获取数据的数据结构.我们可以简单理解为:索引的目的在于提高查询效率. 2.原理 索引的数据结构是B+树,原理图如下 关于B+树的详细介绍,可以参见 ...

  8. 域渗透分析工具BloodHound

    简介:BloodHound是一款将域内信息可视化的单页的web应用程序,是一款在域内进行信息收集的免费工具: Kali中直接命令安装即可 apt-get install bloodhound 打开lo ...

  9. Rocket - debug - TLDebugModuleInner - COMMAND

    https://mp.weixin.qq.com/s/Lz_D43YdhbRhiGiyoCBxDg 简单介绍TLDebugModuleInner中COMMAND寄存器的实现. 1. COMMANDRe ...

  10. Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)

    745. 前缀和后缀搜索 给定多个 words,words[i] 的权重为 i . 设计一个类 WordFilter 实现函数WordFilter.f(String prefix, String su ...