角色权限控制这事儿,说起来简单,但想真正落地不走样,难度比想象中大得多。很多人以为加个 Role 字段就万事大吉,其实关键根本不在字段本身,而是在于“谁在什么上下文里能做什么”——C# 里没有银弹,真得按实际场景选模型,否则后期改起来,比重写还疼。
基于 ASP.NET Core Identity 的声明式权限(适合 Web API / MVC)
这是最贴近 .NET 生态的方案,但麻烦的是,很多人配完 AddIdentity 就以为大功告成。实际上,关键在声明(Claim)粒度和策略注册的时机。
ClaimsPrincipal是运行时权限判断的唯一依据,所有[Authorize(Roles = "Admin")]或context.User.IsInRole("Editor")都依赖它,千万别绕过它自己去查数据库。- 权限不应只靠
Role声明,建议用细粒度Claim,比如new Claim("Permission", "Order:Delete")。这样能避免角色爆炸——什么“超级编辑”“临时审核员”之类的角色名,越堆越乱。 - 自定义策略必须在
Program.cs里注册,而且RequireClaim/RequireAssertion要早于UseAuthentication(),否则中间件根本不会生效。 - 举个例子:检查用户是否拥有某资源的操作权,不能只看角色,得结合
Resource参数,实现一个IAuthorizationHandler才是正解。
基于策略模式的手动权限校验(适合 WinForms / WPF / 后台服务)
没有 HTTP 上下文或 Identity 中间件时,最危险的做法就是硬编码 if (user.Role == "Admin")——权限逻辑散落在各处,改一个功能要翻十处代码,维护成本直线上升。
- 把权限判断抽成接口,比如
IPermissionService.CanAccess(string resource, string operation),实现类可以对接数据库、配置文件甚至远程鉴权服务。 - 避免用字符串硬编码权限名,用
public static class Permissions { public const string User_Read = "User:Read"; }统一管理,清晰又安全。 - 在 WinForms 中,控件级隐藏或禁用,别在每个
Button_Click里重复判断。应该在Form.Load或 ViewModel 初始化时,批量绑定Enabled状态,高效且不易出错。 - 注意线程安全:后台服务中如果权限数据缓存在内存(如
ConcurrentDictionary),更新权限后必须触发清除或版本号刷新,否则脏读可能导致越权。
RBAC 模型落地时最容易出问题的三个地方
RBAC(基于角色的访问控制)听着很规范,但在 C# 项目里常因设计偏差直接退化成“伪 RBAC”。
Role表和Permission表之间必须是多对多,千万别用逗号分隔的字符串字段存权限列表——没法索引、无法原子增删、SQL 查询反人类。- 用户-角色关系不能只存一张表,得有有效期字段(
ValidFrom/ValidTo)和启用状态(IsActive),否则离职人员的权限根本无法及时冻结。 - 权限继承要谨慎:A 角色继承 B 角色,不等于 A 自动获得 B 的所有权限。应该走显式授权链(比如
RoleInheritance关联表),否则审计时根本说不清权限来源。 - 别在 Entity Framework 的
OnModelCreating里给RoleId和PermissionId单独建索引——联合索引HasIndex(r => new { r.RoleId, r.PermissionId })才能加速查询,事半功倍。
权限变更后如何实时生效(而非等 Token 过期)
JWT 有个先天缺陷——Token 一旦签发,权限就固化了。用户改了角色或权限,前端还在用旧 Token,后端不做干预,就会持续越权或拒访。
- 短期方案:Token 里加入
jti(唯一 ID),后端维护一个 Redis 黑名单,每次鉴权前先查redis.Exists("jti:{token_jti}"),简单有效。 - 长期方案:改用 reference token(引用令牌),每次请求都调用
IntrospectionEndpoint实时验证,适合高安全要求系统,比如金融后台。 - 客户端主动刷新不可靠:别指望前端监听到“权限变更”就自动登出重登。应该由后端在关键操作(如
UpdateUserRoles)后,主动使对应用户的全部 Token 失效——通过清空其UserId相关的 Redis key 前缀。 - 调试时注意:ASP.NET Core 默认 JWT 不校验
nbf(Not Before)和exp之外的字段。如果自定义了CheckSignature或ValidateAudience,必须手动开启ValidateIssuerSigningKey = true,否则权限被篡改也毫无察觉。
权限设计从来不是加几个 if 就完事的事,真正难的是边界——比如“同一用户在不同租户下权限不同”“某个按钮既要校验权限又要校验数据归属”。这些地方没抽象好,后面加个新租户就得改半个项目,那才是真正的噩梦。