ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ASP.NET Core RBAC权限管理:五张表与过滤器实践

ASP.NET Core RBAC权限管理:五张表与过滤器实践 先说个场景新系统开发到一半产品经理丢过来一句“我们需要权限管理”。这句话我听了太多次但每次落到.NET项目里情况都不太一样。有的团队直接在Controller里写if (user.IsAdmin)有的花两周时间集成一个重型权限框架最后被框架的配置和黑盒行为折腾到怀疑人生。我自己在多个.NET项目里沉淀了一套RBAC权限系统设计核心思路很简单五张表、一个权限校验过滤器、一套权限码命名规范复制到任何ASP.NET Core项目里都能跑。今天把这套东西完整拆开讲从数据库建模到请求校验链路从缓存设计到落地踩坑全部摊开说。这篇内容适合谁看第一类是准备给内部管理系统做权限控制的后端开发第二类是正在设计权限模块的架构师第三类是手上有个老业务系统、想低成本接入RBAC的项目成员。往下看之前先给你交个底这套设计不追求大而全它解决的是“用户能不能访问某个功能”和“用户能不能看到某个按钮”这两个核心问题数据行级权限和字段级权限在第7章单独讲扩展思路别担心。1. 模型选型为什么我坚持用平级RBAC而不是角色继承1.1 权限需求最常见的三种形态做权限系统之前先看看需求到底长什么样。据我观察90%的业务系统权限需求逃不出三种形态菜单级权限A角色登录后看到“订单管理”B角色看不到这是最基础的门禁控制。操作级权限用户能看到“用户管理”菜单但“删除用户”按钮对他不可见接口也不能调。数据级权限同样是查看订单区域经理只能看本区域的总部能看到全部这已经超出RBAC的标准范畴。前两种形态是RBAC的主场也是这篇文章的核心。第三种形态很多人想用RBAC硬解结果把权限表搞得无比复杂最后还不一定对。我在第7章会讲在RBAC基础上怎么低成本扩展数据隔离这里先按下不表。第一次接触权限系统的人特别容易在第三种需求上投入过多注意力一上来就设计“数据范围字段、规则引擎、策略解析”结果第一版就卡死在理论上。我建议先跑通RBAC再考虑数据级别把简单问题复杂化。1.2 RBAC0和角色继承的取舍RBAC模型其实分好几个版本RBAC0是基础三元组用户-角色-权限RBAC1加了角色继承RBAC2加了职责分离约束RBAC3是两个都有。市面上很多教程一上来就把RBAC1的角色继承讲得很炫比如“部门经理继承员工的权限再额外加审核权”听起来很符合组织架构。但我在实际项目里吃过亏。做个集团权限的时候发现一个“子公司管理员”要继承财务、人事、行政三个角色的部分权限还要排除其中两个权限点RBAC1的继承关系完全救不回来最后只能回到平级角色新建一个角色手工把权限点挨个勾选出来。所以我现在的方案是核心模型只用RBAC0角色之间完全平级。权限配置人员看到的是“这个角色勾选了多少权限点”而不是“这个角色从哪个角色继承了权限又额外加了几个再去掉了几个”。平级角色的逻辑清楚、审计方便、不会出现继承链断了导致权限神秘消失的问题。1.3 这套方案的适用边界再好的工具也有边界这套RBAC设计适合以下场景后台管理系统、内部OA、运营平台权限粒度到按钮级就够。中小体量的SaaS产品租户内部角色数量在几十个以内。单体.NET应用或ASP.NET Core应用权限校验逻辑集中在一个服务内。下列情况我建议你先别直接用RBAC硬套跨系统的统一授权中心那不是RBAC能解决的问题要的是SSO加集中策略下发。租户间权限规则差异极大的平台可能需要独立的策略引擎。强数据隔离场景例如省经理只能看本省数据、客户经理只能看自己名下客户这类需求必须在查询层做数据范围控制不能只靠RBAC。我见过一个团队把RBAC的角色表做成了组织架构树把“部门”“岗位”“角色”混在一起权限审核的时候根本分不清当前登录人到底因为哪个身份获得了权限。模块职责一旦混了后面排查问题就变成侦探游戏。2. 五张核心表的设计权限点、角色、用户到底怎么建模2.1 核心表结构与字段说明RBAC的经典落地就是五张核心表再加一张可选表。直接看结构表名作用关键字段Users用户表Id、UserName、DisplayName、PasswordHash、StatusRoles角色表Id、Code、Name、Description、IsSystemPermissions权限点表Id、Code、Name、ParentId、Type、Sort、StatusUserRoles用户角色关联表Id、UserId、RoleIdRolePermissions角色权限关联表Id、RoleId、PermissionIdUserPermissions可选用户直接授权表Id、UserId、PermissionId、IsGranted为什么权限点表统一叫Permissions而不是分成菜单表、按钮表、接口表因为前端渲染菜单、后端接口鉴权、按钮显隐判断需要的都是同一份权限数据。我见过分表的方案菜单一张表、按钮一张表、接口一张表结果同步起来非常痛苦前端菜单关联了按钮按钮又关联接口中间对不齐就出现“菜单能看见但接口调不通”的怪问题。统一成一张权限点表用Type字段区分资源类型所有模块都拿到同一把权限码去匹配反而简单。具体Type可以这样定义1菜单用于前端路由和菜单树渲染。2按钮用于前端控制增删改查等操作入口。3API接口用于后端过滤器鉴权。还有一种扩展类型4代表数据范围。这是第7章讲数据级权限时需要的预留位。现在用不到也可以先留着避免后面改动表结构。关键字段有几个坑需要提前说明Permissions.Code必须唯一且一旦定义最好不要改。项目里的权限码会散落在代码的Attribute、前端的路由守卫、权限管理后台的配置里改一个Code等于全网重构。Permissions.ParentId用来形成权限树。比如“用户管理”菜单的权限点是system:user:view它的子权限是“新增用户”“编辑用户”“删除用户”通过ParentId挂进来。Status字段用来做软删除。权限点被角色关联表大量引用硬删会牵连作用户角色数据所以删除操作只更新状态不删行。另外UserPermissions这张可选表建议默认建上哪怕第一版用不到。实际业务经常出现“用户张三本来是普通员工但这周要临时审核老板的报销单”你不想为了一个临时权限去改整个角色的权限集合直接用UserPermissions给张三单独加一个授权即可。表里的IsGranted字段还可以实现“某个角色有这个权限但某个人被排除”优先级高于角色权限。2.2 权限码命名规范把Code当身份证管起来权限码是整套权限系统的“身份证”命名规范必须在一开始定好。我用的是三段式{模块}:{功能}:{操作}示例system:user:viewsystem:user:createsystem:user:deleteorder:list:vieworder:list:audit这里有几个约定要落地模块名是最顶层菜单的英文小写命名一个系统里模块名不能重复。操作动词固定用view、create、update、delete、export、import、audit。如果产品经理提了一个“转移负责人”的操作不好意思那基本等于update别再造新动词。按钮权限和接口权限用同一个Code前端按钮判断一个字符串后端Filter校验同一个字符串。比如删除用户按钮的Code是system:user:delete后端接口DeleteUser上也标注[Permission(system:user:delete)]。为什么这么严格因为权限点数量会随业务快速增长。小系统几十个权限点还能靠人脑记做大了以后没有规范就会出现system_user_del、userDelete、delete_user这种混乱命名管理后台里的权限树会变成灾难现场。我参与过的一个数据平台项目就是因为前期没管Code命名后面光做权限码的迁移就花了三个版本。2.3 EF Core实体与关系配置下面直接给可运行的实体定义。以.NET 6/7/8 EF Core为例我习惯用long做Id理由是在团队协作中long型主键的扩展性比int好后续要接缓存、消息队列也不会撞类型。public class User { public long Id { get; set; } public string UserName { get; set; } public string DisplayName { get; set; } public string PasswordHash { get; set; } public int Status { get; set; } public DateTime CreatedAt { get; set; } public ICollectionUserRole UserRoles { get; set; } } public class Role { public long Id { get; set; } public string Code { get; set; } public string Name { get; set; } public string Description { get; set; } public bool IsSystem { get; set; } public ICollectionUserRole UserRoles { get; set; } public ICollectionRolePermission RolePermissions { get; set; } } public class Permission { public long Id { get; set; } public string Code { get; set; } public string Name { get; set; } public long? ParentId { get; set; } public int Type { get; set; } public int Sort { get; set; } public int Status { get; set; } }关联实体public class UserRole { public long Id { get; set; } public long UserId { get; set; } public long RoleId { get; set; } public User User { get; set; } public Role Role { get; set; } } public class RolePermission { public long Id { get; set; } public long RoleId { get; set; } public long PermissionId { get; set; } public Role Role { get; set; } public Permission Permission { get; set; } }FluentAPI配置要点如下重点是级联删除行为modelBuilder.EntityUserRole() .HasKey(ur ur.Id); modelBuilder.EntityUserRole() .HasOne(ur ur.User) .WithMany(u u.UserRoles) .HasForeignKey(ur ur.UserId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.EntityUserRole() .HasOne(ur ur.Role) .WithMany(r r.UserRoles) .HasForeignKey(ur ur.RoleId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.EntityRolePermission() .HasKey(rp rp.Id); modelBuilder.EntityRolePermission() .HasOne(rp rp.Role) .WithMany(r r.RolePermissions) .HasForeignKey(rp rp.RoleId) .OnDelete(DeleteBehavior.Cascade);如果你不喜欢FluentAPI也可以用数据注解但关联实体的联合唯一约束比如UserId RoleId不能重复还是建议在FluentAPI里配数据注解表达不了。实际配置里我还会给UserRoles加上联合唯一索引(UserId, RoleId)给RolePermissions加上(RoleId, PermissionId)。这一步很关键否则权限管理后台重复提交表单会插入重复的关联记录到时你排查“为什么权限越删越多”会卡很久。3. 权限校验链路从登录到请求放行代码落在哪里3.1 一次请求需要经过的权限判断先画一下整条链路用文字描述不画图了文字反而更清楚用户登录成功系统根据UserId查出所有角色再查出这些角色包含的全部权限Code集合写入缓存并把Code集合返回给前端。前端拿到权限Code集合后渲染菜单树、控制按钮显隐、做路由守卫。用户点击某个按钮前端发起API请求。后端首先执行身份认证确认用户是已登录状态。权限过滤器读取当前Action上标注的权限Code查一下用户缓存里的权限集合是否包含这个Code。包含则放行不包含返回403。这条链路的重点在于前端控制只是“体验优化”真正的安全边界必须由后端把关。前端隐藏了删除按钮不意味着用户不能手动发一个删除请求后端权限过滤器必须拦得住。3.2 自定义PermissionAttribute在ASP.NET Core里做权限拦截最直观的入口是自定义Attribute。先定义PermissionAttribute[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple true)] public class PermissionAttribute : Attribute { public string Code { get; } public PermissionAttribute(string code) { Code code; } }用法很简单直接挂在Controller或Action上[HttpGet(users)] [Permission(system:user:view)] public IActionResult GetUsers() { return Ok(); } [HttpPost(users/{id}/delete)] [Permission(system:user:delete)] public IActionResult DeleteUser(long id) { return Ok(); }为什么选Attribute而不是在某个配置中心统一维护接口和权限码的映射因为Attribute直接写在接口旁边写代码的人和代码审查的人一眼就能看到“这个接口需要什么权限”。你在另一个配置文件里配映射改接口时很容易忘记同步配置而且配置文件的查询效率也不如反射元数据来得直观。3.3 用IAsyncAuthorizationFilter统一拦截接下来是关键代码。定义一个过滤器在请求进入Controller之前完成权限校验public class PermissionAuthorizationFilter : IAsyncAuthorizationFilter { private readonly IPermissionChecker _checker; public PermissionAuthorizationFilter(IPermissionChecker checker) { _checker checker; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var endpoint context.HttpContext.GetEndpoint(); var attribute endpoint?.Metadata.GetMetadataPermissionAttribute(); if (attribute null) { // 没有标注Permission的接口不做权限拦截 return; } var userIdValue context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrWhiteSpace(userIdValue)) { context.Result new UnauthorizedResult(); return; } var hasPermission await _checker.HasPermissionAsync(long.Parse(userIdValue), attribute.Code); if (!hasPermission) { context.Result new ForbidResult(); } } }这段代码里有几个细节值得解释GetEndpoint()能拿到当前请求路由到的Action元数据从里面读取Attribute这套机制在Controller和Endpoint Routing模式下都有效。没标注[Permission]的接口直接放行这是一个强烈约定所有接口要么标注权限码要么显式标注[Authorize]声明它仅需登录即可访问。否则开发时忘标一个权限码接口就悄无声息地裸奔了。返回403用ForbidResult()返回未登录用UnauthorizedResult()这个区分很重要前端根据状态码决定是跳登录页还是提示无权限。有人会问为什么不用ASP.NET Core自带的IAuthorizationRequirement加AuthorizationHandler我承认那套Policy机制很强大适合复杂授权策略但RBAC的校验本质就是“判断Code是否在权限集合里”用Filter完全够用。写出来的代码短、直白新人接手也能快速看懂。以后如果真要升级到ABAC再在Filter里加入条件判断也不冲突。3.4 IPermissionChecker的实现上面代码依赖IPermissionChecker这是权限校验的核心服务public class PermissionChecker : IPermissionChecker { private readonly IPermissionCache _cache; private readonly IUserRoleRepository _userRoleRepository; public PermissionChecker(IPermissionCache cache, IUserRoleRepository userRoleRepository) { _cache cache; _userRoleRepository userRoleRepository; } public async Taskbool HasPermissionAsync(long userId, string permissionCode) { // 内置管理员角色拥有全部权限 if (await _userRoleRepository.IsInRoleAsync(userId, admin)) { return true; } var codes await _cache.GetPermissionCodesAsync(userId); return codes.Contains(permissionCode); } }注意这里把“超级管理员”的判定放在最前面不走缓存也不走权限集合。下一节专门解释这么做的原因。3.5 超级管理员为什么单独判断很多人的第一反应是给超级管理员角色勾选所有权限点让admin角色拥有全部权限。但这么做有一个隐藏风险权限表新增一个权限点时如果管理员忘了给admin角色同步勾选超级管理员反而没有权限新功能一上线管理员先打不开还得排查半天。改用代码单独判断是不是admin角色后不管以后权限点加多少个管理员始终拥有全部权限。这里补充一个保护规则内置的admin角色不允许被删除不允许取消IsSystem标记管理后台至少要有一个不能动的角色兜底层级权限。4. 挂进ASP.NET Core管道模块化与接入老项目4.1 推荐的项目分层权限模块不要散落在业务代码里建议单独划一个边界。我的习惯分层如下src/ ├── Acme.Core.Domain 实体、枚举、权限常量 ├── Acme.Core.EntityFrameworkCore DbContext、实体映射、迁移 ├── Acme.Application.Permission IPermissionService、IPermissionChecker ├── Acme.Infrastructure.Cache 缓存实现 └── Acme.Web 控制器、过滤器、Filter注册重点说下为什么要拆两个接口IPermissionService负责权限配置比如给角色分配权限、给用户分配角色、查询用户权限树。它走数据库调用频率低。IPermissionChecker负责运行时鉴权判断某用户是否有某权限。它主要走缓存调用频率极高每个请求都可能触发。拆开的好处是读写分离。写权限配置的代码不会混进高频校验路径也方便单元测试测PermissionChecker时只需要Mock缓存和角色仓库不需要准备数据库。4.2 DI注册与Filter挂载在Program.cs里完成依赖注入和过滤器注册builder.Services.AddScopedIPermissionService, PermissionService(); builder.Services.AddScopedIPermissionChecker, PermissionChecker(); builder.Services.AddSingletonIPermissionCache, PermissionCache(); builder.Services.AddControllers(options { options.Filters.AddPermissionAuthorizationFilter(); });过滤器注册的核心点是必须全局注册不能只加到控制器或Action上。因为权限规则是全系统通用的安全策略局部注册容易漏一旦漏掉一个Action那个接口就是裸奔的。如果你的项目里权限过滤器需要依赖注入options.Filters.AddT()这种泛型注册方式在.NET 6是支持的框架会从DI容器解析类型。如果用的还是老版本可以考虑改成TypeFilterAttribute或手动在FilterDescriptors里装配原理一样。4.3 老项目接入的最小步骤给已上线的老项目加RBAC千万不要先想“一步到位重构”。我推荐一个低风险的接入步骤通过EF迁移在现有数据库里创建五张核心表。写一个初始化脚本内置admin和default两个角色把需要权限控制的菜单和按钮录入Permissions表。在登录接口的成功返回包里追加一个权限Code集合字段。全局注册PermissionAuthorizationFilter。逐个Controller去加[Permission]从核心模块开始不需要一天全改完。前端根据权限Code集合先接菜单渲染再做按钮级v-if。这里有个容易被忽视的经验全局注册过滤器后没有加[Permission]的接口会直接放行所以系统不会突然锁死很多人会因此以为“权限加上去了”实际上还没有。必须把“每个接口要么有权限码、要么明确是公共接口”作为Code Review的硬性检查项持续推动5、6步落地。5. 缓存策略权限校验快得可感知改权限却要秒级生效5.1 为什么权限判断需要独立缓存层如果不做缓存每个API请求都会查一次用户角色关联表、再Join角色权限关联表。权限表小的时候无所谓但系统用户上五千、权限点上三百之后数据库的查询压力会明显反映在接口延迟上。我实际测过的数据可以给你参考一个数据集成项目里权限校验不缓存时平均耗时20ms加缓存后平均不到1ms。20ms对单个接口不致命问题是现在的页面都是接口聚合的一个页面并行调五六个接口权限校验就要占到一两百毫秒体感卡顿就是这么攒出来的。5.2 缓存结构设计与实现单体部署直接用IMemoryCache多实例部署建议用Redis因为Redis可以跨实例共享缓存权限变更后一台节点清缓存其他节点也能感知通过发布/订阅或统一失效版本。缓存结构很简单Key: perm:user:{userId} Value: HashSetstring // 权限Code集合用一个防并发的版本号辅助失效是实现动态权限秒级生效的关键public class PermissionCache : IPermissionCache { private readonly IMemoryCache _cache; private readonly IPermissionRepository _repository; private static readonly Guid _version Guid.NewGuid(); public async TaskISetstring GetPermissionCodesAsync(long userId) { var key $perm:user:{userId}:v{_version}; return await _cache.GetOrCreateAsync(key, async entry { entry.AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(10); return await _repository.GetPermissionCodesAsync(userId); }); } public void RefreshAll() { // 注意这里不能直接改动上面的_version因为它是static readonly // 生产环境建议把版本号存入Redis或MemoryCache的一个键 } }实际生产里版本号要能动态更新。可以用一个ConcurrentDictionary保存“全局权限版本号”每次权限变更时版本号自增然后缓存Key里带上版本号。版本一变所有旧Key全部失效不需要维护“某个角色下有哪些用户”的映射关系。5.3 权限变更后缓存如何失效三种最常见的权限变更场景给角色分配或回收权限影响该角色下所有用户清掉这些用户的缓存。给用户分配或回收角色只影响该用户清掉该用户缓存。用户被禁用清掉该用户缓存并考虑在登录态层面阻止其后续请求。用全局版本号方案后不需要精确维护用户列表一次全局版本变更就可以让所有用户缓存失效。代价是权限变更后的瞬时请求会集体回源数据库如果系统有十万级用户瞬间回源压力会比较大。更平滑的做法是“延迟双删”或“本地缓存加Redis发布订阅”这些高级玩法可以等业务规模真到了再上前期不要加复杂度。5.4 必须避开缓存黑洞这里说的黑洞是指权限变更后缓存长期刷新不出来的问题。我见过一个团队把用户权限缓存设置成“永不过期”然后权限管理后台只改数据库。结果就是角色权限改了用户怎么刷新都还是旧权限最后他们只能在发布站点的时候重启应用来清缓存。我给一个实用建议任何权限变更都要在同一个服务方法里完成“改数据库 刷新缓存”最好包在事务里。要么都成功要么都失败避免数据库和缓存不一致。再把全局版本号这个兜底机制加上双保险之后基本不会出“改完权限半小时不生效”的问题。6. 上线最容易翻车的六个权限细节6.1 权限码散落成字符串重构一改全崩我见过很多项目里[Permission(system:user:delete)]的字符串写得到处都是没有一个地方统一管理。等到权限码改名的时候就得全文替换漏一个就是一个静默漏洞某个接口就突然变成任何人都能访问了。解决办法是建一个静态权限常量类public static class Permissions { public const string UserView system:user:view; public const string UserCreate system:user:create; public const string UserUpdate system:user:update; public const string UserDelete system:user:delete; }所有代码引用常量不再出现裸字符串。这个习惯成本极低收益极高强烈建议从第一天就执行。6.2 前端v-if隐藏了按钮后端却不校验这是权限系统里最危险的翻车点之一。前端隐藏删除按钮只是“用户体验”不代表请求不会被构造出来。只要后端接口没有权限校验任何人随便用工具发一个删除请求就能删数据。前端权限控制是让用户“看不到、点不到”后端权限校验才是安全边界。这俩缺一不可但后端的优先级永远高于前端。6.3 超级管理员权限点没配全登录后一脸懵前面已经反复强调了admin角色要用代码单独判断不能依赖“勾选全部权限点”。如果你正处在从零搭建的阶段建议直接把这条规则写进需求文档里避免后面每次新增权限点都要给管理员角色补一遍全选。6.4 权限变更后缓存和数据库不一致改完角色权限数据库是对的但用户缓存里还是旧权限。这个现象在刚上线时特别容易遇到因为很多团队只做了“改数据库”这一步忘了“清缓存”。排查半小时最后发现是缓存过期时间还没到。我建议把权限变更和缓存失效封装到一个服务方法里方法名就叫UpdateRolePermissionsAsync内部逻辑是改数据库 更新全局版本号。所有调用方只用这一个入口不要允许任何人绕过服务直改关联表。6.5 删除权限点不级联角色关联表残留孤儿数据删除一个父权限时如果角色关联表里还残留着对子权限的引用权限树里就会出现孤儿节点管理后台的权限配置界面会非常乱。删除权限的正确顺序是先递归查出所有子权限批量删除RolePermissions里的关联记录再删除UserPermissions里的关联记录最后把权限本身做软删除。不要图省事只执行一条DELETE FROM Permissions。6.6 接口路径和权限码两套命名排查问题全靠猜有的项目里菜单表存的是system:user:delete接口路径却写DeleteUser甚至某个Controller改了个路由模板从/system/user变成/user/manage权限码还是原来的。两套命名体系一旦对不上权限排查就要来回翻代码效率极低。约定权限码是全系统的唯一标准。接口路径以后想改随便改但权限码一旦定了就不要变。前端按钮、后端接口、菜单表里关联的都是同一个权限码。7. RBAC之外数据行级权限和字段级权限怎么做7.1 数据行级权限的最小实现RBAC解决的是“能不能访问这个资源”数据权限解决的是“能访问资源的哪一部分数据”。最小可行的实现方案是在用户表或用户扩展表上加数据范围字段比如DepartmentId然后在查询层自动拼接过滤条件。例如订单列表接口要求只能看本部门订单var departmentId await _currentUser.GetDataScopeAsync(); var query _context.Orders .Where(o o.DepartmentId departmentId) .ToList();关键原则数据范围永远从后端登录态里解析绝不信任前端传过来的部门ID。否则用户只要改一下请求参数就能把整个集团的订单拉出来。7.2 字段级权限拆接口比动态序列化更省心字段级权限常见于敏感信息场景比如普通员工查看客户列表时不能看到身份证号、联系电话。处理方案有两种方案A返回结果时动态序列化按权限配置过滤字段。优点是前端接口少缺点是配置复杂过滤逻辑散落在序列化层很容易漏字段。方案B拆成两个接口GetCustomerBasic返回基础字段标注customer:viewGetCustomerDetail返回敏感字段标注customer:detail。前端按权限码调不同接口。我推荐方案B。它的代码可读性高权限模型也不变只是多定义几个权限码而已。做数据安全审计的时候也能清楚看到谁调用了customer:detail接口比动态序列化的黑盒行为靠谱得多。7.3 什么时候才需要真正引入ABAC当业务规则出现大量“请求上下文相关”的条件时RBAC就不够用了。比如“工作时间内普通财务可以审核工作时间外只有财务主管才能审核”“金额超过10万的订单需要部门负责人二次确认”。这些本质上是策略判断和用户角色、时间、金额、部门多个因素相关。真正引入ABAC前先考虑一个务实折中以RBAC做主干的权限门禁条件判断放进业务代码里。比如在订单审核业务逻辑里先调用权限过滤器确认基础权限再判断订单金额和当前时间。这样既不用引入复杂的规则引擎也能满足大部分条件授权需求。我见过不少团队一上来就要设计ABAC规则引擎结果业务需求没几个光维护规则配置就消耗了大量人力。现实经验是先跑通RBAC遇到条件授权再局部扩展别为了“架构先进”提前引入整套复杂度。我个人实际走下来这套RBAC设计最难的部分不是表和代码而是权限码的规划。表结构随时可以改权限码第一次没规划好后面只能靠人肉治理。所以动手写代码之前先花一个小时把模块拆清楚、操作动词定标准这个时间花得绝对值。最后再分享一个我自己常用的自查动作每上线一个新接口都习惯性地看一眼Controller的Action上有没有[Permission]是权限码不是裸字符串然后顺手拿一个普通角色和一个admin角色各登录一遍去点一次。权限系统没有银弹但这种习惯能帮你避开绝大多数生产事故。
返回列表