DOTNET 鉴权系列- 动态Claim应用
管理员给用户改了角色之后,都是在群里发消息,让人重新登录下系统然后生效角色的,有时候有的用户不太懂,导致没退出,就会说系统有问题,后面我们在DOTNET 鉴权系列- JWT 会话撤销与主动登出 把 JWT 的踢人做完之后,暂时解决了,管理员直接就在后台给指定用户全部退出了,其实仔细想想虽然加了主动登出还是有点不太合理,改了角色,为啥还要等用户重新登录?其实这一切是因为我们现在颁发的token中Claim存储了角色信息。然后有些业务都是取这个里面的角色信息做逻辑判断。

所以就面临9 点登录,Token 里 Role = Admin。10 点管理员把他改成普通用户。Token 还拿着,下午去调 [Authorize(Roles = "Admin")] 的接口,照样能过。就出现库已经改了,凭证没改,但是JwtBearer 鉴权时验的是Token,Token上写着 Admin。
一开始以为是 Identity 模块没更新。后来发现根本不是模块的关系。Token 里的Claim,进到 ASP.NET Core 之后,就会变成 HttpContext.User 身份。使用权限时 [Authorize(Roles)]、User.IsInRole 读的都是它里面的内容,不读数据库。由于Token是旧的,而身份也是从token解析出来的,导致角色依然还是修改角色之前的内容。
如果你理解了上面描述的信息应该就能知道我开头说的,为什么改了角色,让人重新登录下,后续进行主动退出,后面参考了一些比较优秀的项目的思路和实现。捋清楚之后在这里我把 Dynamic Claims 分享出来。直接用比较通俗的大白话描述一下
Token 就像一张工卡,卡上写着你当时的身份,比如普通开发。但这张卡上的身份,只在发给你那一刻是准的,之后就算你升职了,卡上也不会自动更新。
所以每次你刷卡进门发请求时,门禁客户端不会傻乎乎地只看卡上写的什么,而是去查一下最新的员工信息,发现你已经升职成经理了,就直接按经理的权限放行。
如果既想角色立刻变,又想 Token 完全自包含、服务端永远不查库。立刻生效,这是不可能的。
1.Claim的作用
在没有做这个需求之前之前我对 JWT 和 User的理解比较浅,也没有很深入的去梳理他们的关系,我们颁发一个token,对应系统里面一个登录用户。其实 Jwt干的活很简单——当token验签通过了之后,就把 Payload 里的 Claim 对应成系统里面的一份 ClaimsPrincipal,然后挂到 HttpContext.User 上。

可以这么理解:一个 Token 对应一个 Identity,但这个 Identity 不是 Token 自带的,而是每次请求时服务端重新构建的,下面是对应关系
| JWT Payload | 系统内部Identity | 备注 |
|---|---|---|
sub 或 unique_name |
`User.Identity.Name | 当前是谁 |
role: Admin |
User.IsInRole("Admin") |
判断“是不是管理员 |
session_id |
User.FindFirst("session_id") |
取出会话编号 |
对比之后我们知道Token 里的 Role和应用里的身份默认是同一份数据。[Authorize(Roles = "Admin")] 不是去用户表里问他现在是不是 Admin,是问 这次请求的 Principal 上有没有这条 Claim。
上面的名字只是为了看懂流程。具体哪个 Claim 映射成 Identity.Name、哪个算 Role,由 NameClaimType、RoleClaimType 和 Jwt 的映射配置决定。项目里一旦定好就别来回换,否则很容易出现 Token 里明明有 role,User.IsInRole 却返回 false以及其他的诡异Bug。
下面我们重新认识下比较容易混乱的关键字:
-
Claim:一条身份数据,例如
name=admin、role=Admin。 -
ClaimsIdentity:一组 Claim,再加认证类型、Name/Role ClaimType 等信息,代表一份具体身份。
-
ClaimsPrincipal:Identity 的容器。ASP.NET Core 最终把它放到
HttpContext.User。public abstract class HttpContext { public abstract ClaimsPrincipal User { get; set; } }
所以从原理上来说,动态更新角色不是修改token的Claim,而是修改当前Token请求 ClaimsPrincipal 里的 ClaimsIdentity。先删旧 角色Claim,再加最新 角色 Claim。
在会话撤销的那篇分享说过,JWT 验签证明的是签发方、有没有过期、有没有被改。token中带的 Claim,证明的只是签发那一刻服务端认为他是 Admin以及这些claim有没有被中途篡改。不证明现在他还是不是 Admin。
可能你会和我有一样的疑问。既然会过时,那为啥还要把角色塞进token里呢?后面我也查了相关资料和这么做的理由,主要如下。
- 鉴权无状态Token 里塞角色,用空间换时间。把用户身份信息缓存在 Token 里,这样网关、API 拿到 Token 就能直接判断权限,不用每次都去查数据库。
- .NET Core 的整个鉴权管道就是围绕 Claims 设计的,
[Authorize(Roles = "Admin")]、User.IsInRole、IPrincipal都读 Claim,压根没有去数据库查角色这个动作。中间件负责把 Token 解析成 ClaimsIdentity,后续所有鉴权逻辑都只认 Claims。 - 微服务模式下合同、订单服务往往没有用户表,只能信 Token 里面的身份。
所以角色放进 Identity中,是 Cookie / JWT 的默认做法。但是代价就是角色变了,Token中的内容不会自己变。
2.为什么不直接查库?
其实就算我也认同上面的理由能站得住脚,还是在内心仍然觉得,可以使用最直接的写法在每个业务接口里自己查
var roles = await roleRepository.GetRolesAsync(userId);
if (roles.Contains("Admin"))
{
// to do search workorder
}
这样肯定能用,因为我曾经在前司做用户中心的时候,就是这样的写法,但我们将每个接口都得重复查角色、写判断的逻辑封装 为接口给到各个微服务调用了,那时候没有考虑到框架设计的特性 , 准确的说,根本没用上框架提供的鉴权认证的这个架构,系统中出现了两套身份体系,现在回想就是4不像
- 框架认为他是 Admin,因为 Principal 上还是旧 Role。
- 业务代码认为他不是 Admin,因为刚从数据库查到的是 User。
可以说明最麻烦的地方,不是能不能查库,是不能让框架和业务各认一套身份。而Dynamic Claims 的处理方式是:
在授权之前统一查一次,把最新角色写回当前
ClaimsIdentity。这样 Controller 不用自己查,原来的[Authorize(Roles)]、User.IsInRole、权限 Provider 也不用换用法,使用同一个HttpContext.User。
尽量不要Controller 各自查库,各自判断,框架里的 User 还是旧的,Dynamic Claims做法是统一查库然后覆盖 ClaimsIdentity 整个授权链路上都看到的是新的身份。
所以实际业务中的可以把它们配合起来:
- Token 里的 Claim 提供登录快照,让系统完成认证并拿到
userId。 - 对角色每次查源,再覆盖当前请求里的 Claim。
3.动手覆盖
捋清楚对应关系,下面如何落地呢?如何配合起来,看下面这个交互流程

下面动手。登录Token该怎么颁发还怎么颁发,Role也在一开始颁发时写进去作为快照参考,反正真正这次请求他是什么角色是以这次请求的 Identity为准。
Demo中用内存字典作为角色表,生产可以换成数据库,前面再挂一层缓存就行了。定义两个简单方法GetRoles 读取、SetRoles 更新。管理员改角色就是改这里。后面覆盖身份的代码就是调用这个
public class DemoUserClaimStore
{
private readonly ConcurrentDictionary<string, string[]> _roles = new(StringComparer.OrdinalIgnoreCase);
public DemoUserClaimStore()
{
_roles["admin"] = ["Admin", "User"];
}
public string[] GetRoles(string username) =>
_roles.TryGetValue(username, out var roles) ? roles : [];
public void SetRoles(string username, string[] roles) =>
_roles[username] = roles;
}
1. 覆盖本次请求的 Role
对于已认证的请求,会把Token 里面对应的旧 Role 全部去掉,按查出的最新角色再挂一遍。
public class DynamicClaimsMiddleware
{
private readonly RequestDelegate _next;
private readonly DemoUserClaimStore _store;
public DynamicClaimsMiddleware(RequestDelegate next, DemoUserClaimStore store)
{
_next = next;
_store = store;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated != true)
{
await _next(context);
return;
}
OverlayRoles(context.User);
await _next(context);
}
private void OverlayRoles(ClaimsPrincipal principal)
{
var identity = principal.Identities.FirstOrDefault();
var username = identity?.Name;
if (identity is null || string.IsNullOrEmpty(username))
return;
foreach (var claim in identity.FindAll(ClaimTypes.Role).ToList())
identity.RemoveClaim(claim);
foreach (var role in _store.GetRoles(username))
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
中间件里面做的事,是改内存里这一次请求的 Identity,不是改 JWT,也不是重新签发。RemoveClaim 和 AddClaim 改的就是 HttpContext.User 上那份对象,也不需要再换一个 Principal 写回去。如果需要部门、租户要更新,也可以用这个方法,角色表存的是什么,这里就往这次请求的身份上贴什么。
要注意中间件放在认证和鉴权的中间,顺序不能放反。先 UseAuthentication(),JwtBearer 把 Token 快照映射到 HttpContext.User 上,中间件再覆盖这份 Identity,最后 UseAuthorization() 看到的才是新角色。
app.UseAuthentication();
app.UseMiddleware<DynamicClaimsMiddleware>();
app.UseAuthorization();

2. 改角色,Token 不用换
管理员改的是角色表,不是 Token。同一个 Bearer 再打一次仅 Admin 的接口,立刻 403。
[HttpPut("users/{username}/roles")]
public IActionResult SetRoles(string username, [FromBody] string[] roles)
{
_claimStore.SetRoles(username, roles);
return Ok(new { username, roles });
}
[Authorize(Roles = "Admin")]
[HttpGet("admin-only")]
public IActionResult AdminOnly() =>
Ok(new { message = "你当前仍是 Admin", user = User.Identity?.Name });
1.POST /login,拿到 Token,里面是 Admin + User。
2.带着 Token 调 GET /admin-only,200。
3.PUT /users/admin/roles,body 改成 ["User"]。
4.同一个 Token 再调 GET /admin-only,403。
5.把 Token 解开看看,里面还是 Admin。但是角色表和内存里的 Identity。
按照这样实现管理员 10 点收回 Admin角色,但是用户还是 9 点那张票,下次请求 Identity 上的 Role 已经被覆盖成普通用户,[Authorize(Roles = "Admin")] 直接就会返回 403。这样没有偏离 .NET CORE中 ClaimsPrincipal 的授权设计,又把旧的快照在真正授权之前改对了,注意要把它和权限点分开,不然容易搅在一起。如果你用的是权限名授权,比如 [Authorize("Books.Create")],权限值存在权限表 / 缓存里,按 userId 查,本来就不依赖 Token 里的 Role,这条路通常不会Token 角色过期。咱们当前这种主要针对解决的是:
User.IsInRole和CurrentUser.Roles
[Authorize(Roles = "...")]
3.还有从
Principal上获取的角色再去拿权限的Provider

浙公网安备 33010602011771号