当前位置: 首页 > news >正文

.NET 10 Claim 身份体系深度解析

前言

身份验证(Authentication)与授权(Authorization)是每一个 Web 应用的安全基础。自 .NET Framework 4.5 起,微软将 WIF(Windows Identity Foundation)全面整合进 .NET,引入了以 Claim 为核心的现代身份体系。时至 .NET 10,这套体系已相当成熟,是构建精细化权限控制不可绕开的基石。

本文从最基础的概念出发,逐层深入,覆盖核心类型、请求管道流转、Claims 映射、动态转换、 策略 授权等完整链路,并在每个环节给出工程实践建议。


一、什么是 Claim?

1.1 核心概念

Claim 是由签发方(Issuer) 对某个实体(Subject) 所作的一种声明,用以描述该实体的某个属性或特征。它描述"主体是什么",而不是"主体能做什么"。

现实世界中最直观的类比是身份证

        签发方(公安局)            实体(张三)│▼签发一张身份证┌─────────────────────────────────┐│  姓名   = 张三                  │ ← Claim(Type="name",    Value="张三")│  出生日期 = 1994-05-01          │ ← Claim(Type="dob",     Value="1994-05-01")│  国籍   = 中国                  │ ← Claim(Type="country", Value="CN")│  签发机关 = 上海市公安局        │ ← Issuer = "上海市公安局"└─────────────────────────────────┘

酒吧场景进一步说明了 Claim 与授权的关系:服务员读取你身份证上的"出生日期"这一 Claim,根据"年龄 ≥ 18 岁"的策略(Policy)决定是否允许购酒。这就是 Claim 驱动授权的完整流程。

1.2 Claim 类的关键属性

// 完整构造函数
public Claim(string type,           // Claim 的语义标识(字段名),通常为 URI 或短字符串string value,          // Claim 的具体值string valueType,      // 值的数据类型,来自 ClaimValueTypes 常量string issuer,         // 签发方(谁说的)string originalIssuer  // 原始签发方(经中转时有意义)
)// 示例
const string issuer = "https://auth.mycompany.com";
var claim = new Claim(ClaimTypes.Email,"alice@example.com",ClaimValueTypes.String,issuer
);

1.3 为什么 Issuer 很重要?

同一个 Claim 类型,来自不同来源的可信度截然不同。应用应该只信任受信任签发方的 Claim:

// ❌ 危险:任何人声称自己是 admin 都被信任
bool isAdmin = context.User.HasClaim("role", "admin");// ✅ 安全:只信任来自指定签发方的 Claim
bool isAdmin = context.User.HasClaim(c =>c.Type   == "role" &&c.Value  == "admin" &&c.Issuer == "https://auth.mycompany.com");

1.4 标准 ClaimTypes 速查

System.Security.Claims.ClaimTypes 预定义了大量标准类型,均以 WS-Federation(Web Services Federation)风格的 URI 表示:

常量名 语义 对应 OIDC(OpenID Connect)短名称
ClaimTypes.NameIdentifier 唯一用户 ID sub
ClaimTypes.Name 显示名称 name
ClaimTypes.Email 电子邮件 email
ClaimTypes.Role 角色 role / roles
ClaimTypes.GivenName given_name
ClaimTypes.Surname family_name
ClaimTypes.DateOfBirth 出生日期 birthdate
ClaimTypes.MobilePhone 手机号 phone_number

二、三层核心 架构

.NET 的 Claim 体系是一个三层嵌套结构:

┌──────────────────────────────────────────────────────┐
│                  ClaimsPrincipal                     │
│           (安全主体,代表"这个人")                  │
│                                                      │
│  ┌─────────────────────┐  ┌─────────────────────┐   │
│  │    ClaimsIdentity   │  │    ClaimsIdentity   │   │
│  │  AuthType="Cookies" │  │  AuthType="Bearer"  │   │
│  │  (用户身份证)      │  │  (API令牌身份)     │   │
│  │  ┌───────────────┐  │  │  ┌───────────────┐  │   │
│  │  │ Claim(name)   │  │  │  │ Claim(sub)    │  │   │
│  │  │ Claim(email)  │  │  │  │ Claim(scope)  │  │   │
│  │  │ Claim(role)   │  │  │  └───────────────┘  │   │
│  │  │ Claim(role)   │  │  └─────────────────────┘   │
│  │  └───────────────┘  │                            │
│  └─────────────────────┘                            │
└──────────────────────────────────────────────────────┘

2.1 ClaimsIdentity:身份证件

ClaimsIdentity 是 claims-based identity 的具体实现,类比一本护照,护照上的每一项信息就是一个 Claim

AuthenticationType 是最关键也最容易被忽视的属性。 它有两个作用:

作用一:决定 IsAuthenticated 的值。 源码实现就是一行判断:

public virtual bool IsAuthenticated => !string.IsNullOrEmpty(AuthenticationType);
// ❌ 常见 Bug:没传 AuthenticationType → IsAuthenticated = false
//    [Authorize] 会直接拒绝,表现为莫名其妙的 401
var identity = new ClaimsIdentity(claims);
Console.WriteLine(identity.IsAuthenticated); // False// ✅ 任何非空字符串 → IsAuthenticated = true
var identity = new ClaimsIdentity(claims, "Cookies");
Console.WriteLine(identity.IsAuthenticated); // True

作用二:标识身份的来源方案。 按约定写认证方案名,便于后续判断身份来源:

// 按认证方案的约定名称填写
var cookieIdentity = new ClaimsIdentity(claims, "Cookies"); // Cookie 登录
var jwtIdentity    = new ClaimsIdentity(claims, "Bearer");  // JWT 令牌
var testIdentity   = new ClaimsIdentity(claims, "TestAuth");// 单元测试// 通过 AuthenticationType 判断身份来源
var authType = User.Identity?.AuthenticationType;
// "Cookies" / "Bearer" / "Google" ...

四种构造方式:

// ① 仅传 Claims(IsAuthenticated = false,通常是 Bug)
var id1 = new ClaimsIdentity(claims);// ② 传认证类型(最常用)
var id2 = new ClaimsIdentity(claims, "Cookies");// ③ 显式指定 Name Claim 和 Role Claim 的 Type
var id3 = new ClaimsIdentity(claims,authenticationType: "jwt",nameType: "preferred_username", // .Name 读哪个 Type 的 ClaimroleType: "roles"               // IsInRole() 查哪个 Type 的 Claim
);// ④ 先空构造,再动态添加
var id4 = new ClaimsIdentity("Bearer");
id4.AddClaim(new Claim(ClaimTypes.Name, "Alice"));
id4.AddClaims(new[]
{new Claim(ClaimTypes.Role, "Admin"),new Claim(ClaimTypes.Role, "Editor"), // 同类型多个 Claim 完全合法
});

关于 Claim.Type 的规则:

Claim.Type 本质是一个普通字符串,技术上可以随意写,框架不做格式校验。它的作用是多值字典的 Key

Claims 集合(可以理解为 List<(string Type, string Value)>):("name",       "Alice")("email",      "alice@example.com")("role",       "Admin")("role",       "Editor")   ← 同一 Type 可以多个 Value("department", "Engineering")

有三个消费方在使用 Claim.Type

  1. 框架内部的 .Name.IsInRole()(根据 nameType/roleType 查找)
  2. 授权策略的 RequireClaim("department", "Engineering")
  3. 业务代码的 User.FindFirstValue("department")

最佳实践:用常量集中管理所有自定义 Type,永远不要写魔法字符串:

public static class AppClaimTypes
{public const string TenantId   = "tenant_id";public const string Department = "department";public const string Permission = "permission";public const string FullName   = "full_name";
}// 写入、读取、策略配置,全部引用同一个常量
identity.AddClaim(new Claim(AppClaimTypes.Department, "Engineering"));
var dept = User.FindFirstValue(AppClaimTypes.Department);
policy.RequireClaim(AppClaimTypes.Department, "Engineering");

2.2 ClaimsPrincipal:安全主体

ClaimsPrincipal 是整个安全上下文,代表"这个人",聚合多个 ClaimsIdentity

多个 Identity 存在的意义:现实中一个人可能同时持有多种证件(护照 + 驾照),用不同 Identity 分开管理,职责清晰:

// 用户身份(已认证)
var userIdentity = new ClaimsIdentity(new[]
{new Claim(ClaimTypes.Name,  "Alice"),new Claim(ClaimTypes.Email, "alice@example.com"),new Claim(ClaimTypes.Role,  "Admin"),
}, "Cookies");// 设备信息(补充上下文,无 AuthenticationType)
var deviceIdentity = new ClaimsIdentity(new[]
{new Claim("ip_address", "192.168.1.100"),new Claim("user_agent", "Mozilla/5.0"),
});var principal = new ClaimsPrincipal(new[] { userIdentity, deviceIdentity });// Claims 属性聚合所有 Identity 的全部 Claim
foreach (var claim in principal.Claims)Console.WriteLine($"{claim.Type}: {claim.Value}");

Identity 主身份的选取规则:

ClaimsPrincipal.Identity 的选取优先级:1. 优先返回第一个 WindowsIdentity2. 没有 WindowsIdentity → 返回第一个 ClaimsIdentity3. 都没有 → 返回 null4. 集合为空 → 抛出 ArgumentNullException(可通过 PrimaryIdentitySelector 属性自定义选取逻辑)

三、Claim 在请求管道中的完整流转

HTTP Request│▼
┌─────────────────────────────────────────────────┐
│         Authentication Middleware               │
│                                                 │
│  1. 读取 Cookie / Bearer Token / 其他凭证       │
│  2. 验证签名、过期时间                          │
│  3. 构建初始 ClaimsPrincipal                    │
│     (Token 里有什么 Claim 就是什么)            │
│  4. 调用 IClaimsTransformation.TransformAsync   │
│     (在这里动态追加/修改 Claims)               │
│  5. 写入 HttpContext.User                       │
└──────────────────┬──────────────────────────────┘│▼
┌──────────────────────────────────────────────────┐
│         Authorization Middleware                 │
│                                                  │
│  读取 HttpContext.User.Claims                    │
│  依次评估 Policy → Requirement → Handler         │
└──────────────────┬───────────────────────────────┘│▼Controller / Endpoint(通过 User.Claims 访问)

在 Controller 中访问 Claims:

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ProfileController : ControllerBase
{[HttpGet]public IActionResult Get(){var userId  = User.FindFirstValue(ClaimTypes.NameIdentifier);var email   = User.FindFirstValue(ClaimTypes.Email);var roles   = User.FindAll(ClaimTypes.Role).Select(c => c.Value);bool isAdmin = User.HasClaim(c =>c.Type   == ClaimTypes.Role &&c.Value  == "Admin" &&c.Issuer == "https://auth.mycompany.com"); // 验证签发方string name  = User.Identity?.Name;string scheme = User.Identity?.AuthenticationType; // "Cookies" / "Bearer"return Ok(new { userId, email, roles, isAdmin, name, scheme });}
}

四、Claims Mapping:解决字段名不一致问题

4.1 问题根源

JWT(JSON Web Token )和 OIDC(OpenID Connect)使用短字段名,而 .NET 内置的 ClaimTypes 常量是长 URI,两者对不上:

JWT Token 里:                   .NET ClaimTypes 常量(实际是 URI):"sub"   → "usr-001"             ClaimTypes.NameIdentifier ="email" → "alice@example.com"     "http://schemas.xmlsoap.org/.../nameidentifier""name"  → "Alice"               ClaimTypes.Email ="http://schemas.xmlsoap.org/.../emailaddress"

4.2 .NET 的默认自动翻译表

.NET 内置一张映射表,解析 JWT 时自动将短名称转换成 URI:

JWT 字段     →   自动映射为(长 URI)
─────────────────────────────────────────────────
"sub"        →   ClaimTypes.NameIdentifier
"name"       →   ClaimTypes.Name
"email"      →   ClaimTypes.Email
"role"       →   ClaimTypes.Role
"given_name" →   ClaimTypes.GivenName

默认情况下,用 ClaimTypes.XXX 常量访问就能找到对应值,因为已经被自动翻译了。

4.3 三种场景及对应做法

场景 A:使用默认映射(适合新项目快速上手)

// 什么都不做,JWT 里的 "sub" 自动变成 ClaimTypes.NameIdentifier
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); // ✅

场景 B:清除默认映射,直接用短名称(推荐现代 JWT 项目)

// Program.cs 启动时清除映射表
JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();// 之后必须用原始字段名,不能再用 ClaimTypes 常量
var userId = User.FindFirstValue("sub");    // ✅
var email  = User.FindFirstValue("email");  // ✅// 下面这行找不到,因为没有映射了
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); // ❌ null

场景 C:外部 IdP(Identity Provider,身份提供商)返回非标准字段

.AddOpenIdConnect(options =>
{options.GetClaimsFromUserInfoEndpoint = true;// 把 IdP 返回的 "preferred_username" 字段保留为同名 Claimoptions.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");// 把 IdP 返回的 "custom_dept" 字段重命名为 "department"options.ClaimActions.MapUniqueJsonKey("department", "custom_dept");
});

自定义 name/role Claim 的映射:

.AddOpenIdConnect(options =>
{options.TokenValidationParameters = new TokenValidationParameters{NameClaimType = "preferred_username", // .Name 读这个字段RoleClaimType = "roles"               // IsInRole() 查这个字段};
});

五、IClaimsTransformation:动态丰富 Claims

5.1 调用时机

每次调用 HttpContext.AuthenticateAsync() 成功后,框架立即调用 TransformAsync不只是登录时,而是每次请求都会触发

每次 HTTP 请求│▼
Authentication Middleware│  Token / Cookie 验证通过│  构建初始 ClaimsPrincipal(仅含 Token 里的 Claim)│▼  ← ⭐ 每次请求都在这里触发
IClaimsTransformation.TransformAsync()│  可以追加、修改 Claims│▼
HttpContext.User(最终版本,含追加的 Claims)

5.2 为什么不在登录时一次性读数据库?

登录时一次性读是可以的,但有时候不够用。核心矛盾是:权限可能在用户登录之后发生变化,而 Cookie/Token 不会自动更新

用户登录│  Cookie 签发,记录 permission = ["read", "write"]││  ← 管理员后台撤销了该用户的 "write" 权限│
下一个请求│  Cookie 里还是有 "write" ← 安全漏洞

IClaimsTransformation 每次请求都重新从数据库(或缓存)加载权限,确保实时性。

两种方案的适用场景:

登录时写入(UserClaimsPrincipalFactory) IClaimsTransformation
数据库请求次数 仅登录时一次 每次请求(需加缓存)
权限实时性 差(Token 过期前不更新) 好(每次反映最新状态)
适合场景 权限几乎不变的系统 权限动态变化的系统

实践建议:两者结合。登录时写稳定信息(userId、name、email),IClaimsTransformation 每次请求从缓存读权限,缓存失效时才真正查数据库:

public class PermissionClaimsTransformation : IClaimsTransformation
{private readonly IPermissionService _svc;private readonly IMemoryCache _cache;public PermissionClaimsTransformation(IPermissionService svc, IMemoryCache cache){_svc   = svc;_cache = cache;}public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal){// ✅ 幂等保护:已经追加过就不重复处理if (principal.HasClaim(c => c.Type == "perms_loaded"))return principal;var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);if (userId is null) return principal;// ✅ 缓存:正常请求命中缓存,不查数据库//    权限变更时主动使缓存失效,下次请求自动更新var perms = await _cache.GetOrCreateAsync($"perms:{userId}",async entry =>{entry.SlidingExpiration = TimeSpan.FromMinutes(5);return await _svc.GetPermissionsAsync(userId);});var extra = new ClaimsIdentity();extra.AddClaim(new Claim("perms_loaded", "true"));foreach (var p in perms)extra.AddClaim(new Claim(AppClaimTypes.Permission, p));principal.AddIdentity(extra);return principal;}
}// 注册为 Transient(每次请求创建新实例)
builder.Services.AddTransient<IClaimsTransformation, PermissionClaimsTransformation>();

六、基于 Policy 的授权体系

这是整个 Claim 体系在授权层面的集大成者,由三个角色协作完成。

6.1 三个角色的职责

角色 类比 职责
Policy 门卫手册上的入楼规则 规则的名字,以及该规则由哪些条件组成
Requirement 某一项具体条件(“需持有工牌”) 描述需要满足什么,是数据容器
Handler 实际执行检查的门卫 判断用户是否满足某个 Requirement,是逻辑容器

6.2 两条核心规则

Policy 内多个 Requirement → AND 语义(全部必须满足)同一 Requirement 多个 Handler → OR 语义(任一满足即可)
Policy "OfficeEntry"│├── HasBadgeRequirement          (AND)│       ├── BadgeEntryHandler    (OR)  → 持正式工牌│       └── TempStickerHandler   (OR)  → 持临时贴纸也行│└── WorkingHoursRequirement      (AND)└── WorkingHoursHandler         → 必须在工作时段

6.3 完整实现

Step 1:定义 Requirement(数据容器)

// 无参 Requirement
public class HasBadgeRequirement : IAuthorizationRequirement { }// 带参 Requirement(参数在构造函数里)
public class WorkingHoursRequirement : IAuthorizationRequirement
{public int StartHour { get; }public int EndHour   { get; }public WorkingHoursRequirement(int start, int end){StartHour = start;EndHour   = end;}
}

Step 2:实现 Handler(逻辑容器)

// 方式一:持正式工牌可进
public class BadgeEntryHandler : AuthorizationHandler<HasBadgeRequirement>
{protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,HasBadgeRequirement requirement){if (context.User.HasClaim(c =>c.Type   == "badge_type" &&c.Value  == "permanent" &&c.Issuer == "https://hr.mycompany.com")){context.Succeed(requirement); // ✅ 满足,放行}// 不调用 Succeed 也不调用 Fail → 弃权,让其他 Handler 继续return Task.CompletedTask;}
}// 方式二:持有效临时贴纸也可进(同一 Requirement 的另一个 Handler)
public class TempStickerHandler : AuthorizationHandler<HasBadgeRequirement>
{protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,HasBadgeRequirement requirement){var expiryClaim = context.User.FindFirst("temp_sticker_expiry");if (expiryClaim is not null &&DateTime.TryParse(expiryClaim.Value, out var expiry) &&expiry > DateTime.UtcNow){context.Succeed(requirement); // ✅ 贴纸未过期,放行}return Task.CompletedTask;}
}// 工作时段检查
public class WorkingHoursHandler : AuthorizationHandler<WorkingHoursRequirement>
{protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,WorkingHoursRequirement requirement){var hour = DateTime.Now.Hour;if (hour >= requirement.StartHour && hour < requirement.EndHour)context.Succeed(requirement);elsecontext.Fail(); // ❌ 明确拒绝,即使其他 Handler Succeed 也无效return Task.CompletedTask;}
}

Step 3:注册 Policy 和 Handler

Requirement 与 Handler 的对应关系靠泛型类型参数自动匹配,无需手动配置:

// Handler 泛型参数 <T> 就是它负责处理的 Requirement 类型
// AuthorizationHandler<HasBadgeRequirement> → 处理 HasBadgeRequirementbuilder.Services.AddAuthorization(options =>
{options.AddPolicy("OfficeEntry", policy =>policy.AddRequirements(new HasBadgeRequirement(),new WorkingHoursRequirement(9, 18)));
});// 全部注册为 IAuthorizationHandler,框架运行时自动按泛型类型匹配
builder.Services.AddSingleton<IAuthorizationHandler, BadgeEntryHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, TempStickerHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, WorkingHoursHandler>();

运行时匹配流程:

IAuthorizationService 从 DI 容器取出所有 IAuthorizationHandler│├── HasBadgeRequirement│     筛选泛型参数为 HasBadgeRequirement 的 Handler:│         BadgeEntryHandler  → Succeed ✅│         TempStickerHandler → 弃权│     任一 Succeed → HasBadgeRequirement ✅│└── WorkingHoursRequirement筛选泛型参数为 WorkingHoursRequirement 的 Handler:WorkingHoursHandler → 当前 14:00 在范围内 → Succeed ✅WorkingHoursRequirement ✅两个 Requirement 全部满足 → Policy 通过 → 请求放行

Step 4:使用 Policy

// 声明式(Attribute 方式)
[Authorize(Policy = "OfficeEntry")]
public IActionResult EnterOffice() => Ok("Welcome!");// 命令式(运行时动态判断,可传入具体资源对象)
public class RoomController : ControllerBase
{private readonly IAuthorizationService _authz;public RoomController(IAuthorizationService authz) => _authz = authz;public async Task<IActionResult> Enter(string roomId){// 第二个参数是资源对象,Handler 里可通过 context.Resource 取到var result = await _authz.AuthorizeAsync(User, roomId, "OfficeEntry");if (!result.Succeeded) return Forbid();return Ok($"Entered room {roomId}");}
}

七、与 ASP.NET Core Identity 的集成

ASP.NET Core Identity 将用户持久化的 Claims 存入数据库(AspNetUserClaims 表),并通过 UserManager<TUser> API 管理:

public class UserClaimsService
{private readonly UserManager<AppUser> _userManager;public UserClaimsService(UserManager<AppUser> userManager)=> _userManager = userManager;public async Task AddClaimAsync(string userId, string type, string value){var user  = await _userManager.FindByIdAsync(userId);var claim = new Claim(type, value, ClaimValueTypes.String);await _userManager.AddClaimAsync(user, claim);}public async Task<IList<Claim>> GetClaimsAsync(string userId){var user = await _userManager.FindByIdAsync(userId);return await _userManager.GetClaimsAsync(user);}public async Task ReplaceClaimAsync(string userId, Claim oldClaim, Claim newClaim){var user = await _userManager.FindByIdAsync(userId);await _userManager.ReplaceClaimAsync(user, oldClaim, newClaim);}
}

登录时 Identity 通过 UserClaimsPrincipalFactory 将数据库 Claims 填入 ClaimsPrincipal。可以继承该工厂注入额外的稳定信息:

public class AppUserClaimsPrincipalFactory: UserClaimsPrincipalFactory<AppUser, AppRole>
{public AppUserClaimsPrincipalFactory(UserManager<AppUser> userManager,RoleManager<AppRole> roleManager,IOptions<IdentityOptions> options): base(userManager, roleManager, options) { }protected override async Task<ClaimsIdentity> GenerateClaimsAsync(AppUser user){var identity = await base.GenerateClaimsAsync(user);// 登录时写入稳定的业务信息(不会动态变化的数据)identity.AddClaim(new Claim(AppClaimTypes.TenantId,  user.TenantId.ToString()));identity.AddClaim(new Claim(AppClaimTypes.FullName,$"{user.FirstName} {user.LastName}"));return identity;}
}// 注册替换默认工厂
builder.Services.AddScopedIUserClaimsPrincipalFactory<AppUser>,AppUserClaimsPrincipalFactory>();

八、完整数据流总览

用户登录│▼
UserClaimsPrincipalFactory.GenerateClaimsAsync()│  从数据库读取稳定信息│  写入 name、email、tenantId 等 Claim▼
Cookie / JWT Token 签发│  Claims 序列化进 Token││  ← 权限可能在此期间变化▼
后续每次请求│▼
Authentication Middleware│  验证 Token,构建初始 ClaimsPrincipal▼
IClaimsTransformation.TransformAsync()│  从缓存/数据库读取实时权限│  追加 permission Claim▼
Authorization Middleware│  评估 Policy → Requirement → Handler│  Handler 查询 ClaimsPrincipal.Claims▼
Controller / Endpoint│  User.FindFirstValue() / User.HasClaim() / User.IsInRole()▼
Response

九、工程最佳实践清单

1. Claim Type 用常量集中管理

所有自定义 Claim Type 定义在一个静态类里,消灭魔法字符串。

2. 构建 ClaimsIdentity 时必须传 AuthenticationType

忘传是导致 401 的常见 Bug。单元测试中随便写一个非空字符串即可(如 "TestAuth")。

3. 明确 nameTyperoleType

如果 Claim 里 Type 是 "roles"(复数),但没有显式设置 roleType = "roles"User.IsInRole() 永远返回 false

4. IClaimsTransformation 做好幂等保护和缓存

每次请求都触发,不加保护会重复追加 Claim,不加缓存会频繁打数据库。

5. 验证 Issuer

授权逻辑中验证 Claim 的 Issuer,防止伪造。

6. 策略(Policy)优于角色检查

[Authorize(Roles = "Admin,Manager")] 改为有语义的 Policy,安全规则自文档化,也更易于单元测试。

7. 单元测试中 Mock ClaimsPrincipal

private static ClaimsPrincipal BuildUser(params Claim[] claims)
{var identity = new ClaimsIdentity(claims, "TestAuth");return new ClaimsPrincipal(identity);
}[Fact]
public async Task Handler_Succeeds_WhenAgeIsValid()
{var user = BuildUser(new Claim(ClaimTypes.DateOfBirth, "1990-01-01"));var ctx = new AuthorizationHandlerContext(new[] { new MinimumAgeRequirement(21) }, user, null);await new MinimumAgeHandler().HandleAsync(ctx);Assert.True(ctx.HasSucceeded);
}

总结

.NET 10 的 Claim 体系以 Claim → ClaimsIdentity → ClaimsPrincipal 三层结构为核心,各层职责清晰:Claim 是最小的信息单元,ClaimsIdentity 是一张带有来源标识(AuthenticationType)的证件,ClaimsPrincipal 是聚合多个证件的安全主体。

在请求管道中,Claims 经由 Mapping 层统一字段名,经由 IClaimsTransformation 动态丰富实时权限,最终交由 Policy/Requirement/Handler 三层结构完成精细化授权决策。

相比传统的角色授权,Claims 驱动的访问控制模型能携带更丰富的身份信息,以更精细的方式表达权限语义,是构建现代 .NET 应用安全架构不可或缺的基础。

http://www.jsqmd.com/news/882119/

相关文章:

  • 微信小程序抓包实战:Charles与Burp组合配置与深度调试
  • 嵌入式多核平台任务分配优化与能耗控制实践
  • OpenHarmony Next与Unity团结引擎环境搭建实战指南
  • 机器学习原子间势能:原理、实战与通用模型选型指南
  • 强化学习硬件加速:QForce-RL量化技术解析
  • DnCNN与DDPM在焊缝超声检测去噪中的原理对比与工程实践
  • 融合机器学习与网络分析:实战解析社交媒体影响力测量框架
  • 真实SRC渗透复盘:从JS校验绕过到密钥泄露的全链路分析
  • x64dbg下载安装与实战调试入门指南
  • 告别TeamViewer:用这3款免费替代软件前,先按这个清单彻底清理Windows
  • 利用窄带测光与机器学习高效筛选星系巨星成员
  • 2026年实测5款免费降ai率工具:高效降低ai率,论文降aigc必备,省时又省力! - 降AI实验室
  • 2026年4月靠谱的防水公司推荐,地下室防水补漏/墙砖空鼓维修/房屋维修/阳台防水补漏/厂房防水补漏,防水服务公司选哪家 - 品牌推荐师
  • 《广东光伏哪家好:排名前五 专业深度测评》 - 服务品牌热点
  • Vision Transformer在径向速度法系外行星探测中的应用与实现
  • 别再死磕光线追踪了!用Unity Shader Graph 5分钟搞定皮肤/玉石SSS次表面散射效果
  • Windows Subsystem for Android深度技术解析:开发者视角的跨平台集成方案
  • Keil C166中xhuge指针与内存模型问题解决方案
  • Unity在Ubuntu上播放本地视频踩坑记:从‘路径无效’到‘编码转换’的完整解决流程
  • FSM-DQN混合控制:仿蚁群机器人集群去中心化空间分离策略
  • 【问题】IDEA import导入的类明明存在却报异常飘红
  • Comba架构:基于双线性RNN的高效序列建模新方法
  • 2026年4月TD6-140钢扣板实力厂家推荐,钢楼承板/压型钢板/钢结构楼承板/镀锌楼承板,钢扣板企业选哪家 - 品牌推荐师
  • Godot逆向工具链:PCK解包与GDScript反编译实战指南
  • Unity ASW风格格斗Shader实战:描边、阴影与受击反馈系统
  • Unity项目发布踩坑记:从Mono切换到IL2CPP,我解决了哪些环境配置问题?
  • 电梯定位新思路:融合物理模型与机器学习,实现高精度连续位置追踪
  • git的使用技巧汇总
  • SLED框架:边缘计算中的LLM推理加速方案
  • 告别黑屏和进度条卡住:深度排查Unity WebGL在360、Chrome等浏览器的兼容性问题