.Net Core 6 API对接OAuth实现用户角色权限映射最佳实践问询
.NET Core 6 对接OIDC SSO实现本地独立角色权限的落地方案
你当前考虑的两个方案都存在明显缺陷,以下是行业通用的成熟实现,同时满足安全、性能、各应用独立维护权限的要求,完全基于.NET Core原生管道实现,不需要额外引入第三方组件。
核心实现思路
首先明确两个基础原则:
- 纯API场景优先使用无状态Bearer Token认证,不要引入Cookie混合认证模式,后者仅适用于服务端渲染的Web应用,用在API场景会额外带来CSRF风险,还会破坏API的无状态特性,给跨域、移动端对接制造不必要的问题。
- 不要自定义独立中间件重写
UserIdentity,直接用JWT Bearer认证官方提供的OnTokenValidated扩展点即可,这个事件仅在JWT通过签名校验、有效期校验、颁发者校验、受众校验等全部默认安全检查后才会触发,不会出现非法token绕过校验进入业务逻辑的问题,比手写中间件安全性高很多。
解决每次请求查库的性能问题,用缓存+和token对齐的过期策略即可,不需要每次请求都访问数据库:
- 缓存key用JWT中携带的全局唯一用户标识
sub声明值,缓存内容为该用户在本应用内的本地用户ID、绑定角色、权限点列表 - 缓存有效期和Access Token有效期完全对齐,比如你的token有效期是1小时,缓存就设1小时绝对过期,token失效时缓存自动同步失效,不会出现权限长期不一致的问题
- 多实例部署场景下用分布式缓存做共享,单实例部署用内存缓存即可,正常场景下99%的请求都会命中缓存,不会产生数据库查询开销,性能和直接从JWT取角色声明几乎无差异
- 后台修改用户角色/权限时,直接根据用户
sub删除对应缓存条目即可,用户下一次请求会自动拉取最新权限,不需要等缓存自然过期,兼顾性能和权限实时性
具体逻辑在OnTokenValidated事件中按以下流程执行:
- 从校验通过的JWT中提取
sub声明值作为外部身份唯一标识 - 优先查询缓存中是否存在该用户的本地权限信息,命中则直接使用
- 缓存未命中时查询本地数据库:如果不存在
sub对应的本地用户记录,按业务需求选择自动注册默认权限账号,或直接返回403提示无应用访问权限;如果存在记录则拉取该用户绑定的所有角色、权限点,写入缓存 - 将本地用户ID、角色、权限点转换为标准
Claim追加到当前ClaimsIdentity中,后续可以直接使用.NET原生的[Authorize(Roles = "xxx")]特性、角色判断API、基于策略的授权做细粒度访问控制,不需要额外改造业务代码
最小实现代码示例(.NET 6)
// Program.cs 中配置认证服务 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "你的IdentityServer4部署地址"; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, // 按你实际的ID4受众配置调整 NameClaimType = "name", RoleClaimType = ClaimTypes.Role }; options.Events = new JwtBearerEvents { OnTokenValidated = async context => { // 提取ID4返回的全局唯一用户标识 var externalSub = context.Principal.FindFirstValue("sub"); if (string.IsNullOrWhiteSpace(externalSub)) { context.Fail("Token中缺少合法用户标识"); return; } // 从DI容器获取缓存实例 多实例部署替换为IDistributedCache即可 var cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); var userPermData = await cache.GetOrCreateAsync($"AppUserPerm_{externalSub}", async cacheEntry => { // 缓存过期时间和Access Token有效期保持一致 cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); var dbContext = context.HttpContext.RequestServices.GetRequiredService<你的应用DbContext>(); // 查询本地用户关联关系 var localUser = await dbContext.AppUsers .FirstOrDefaultAsync(u => u.ExternalProviderSub == externalSub); if (localUser == null) { // 按业务需求处理:自动初始化默认用户/直接返回无权限 return null; } // 查询用户绑定的角色、权限点 var userRoles = await dbContext.UserRoles .Where(ur => ur.UserId == localUser.Id) .Join(dbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.RoleCode) .ToListAsync(); var userPermissions = await dbContext.RolePermissions .Where(rp => userRoles.Contains(rp.Role.RoleCode)) .Select(rp => rp.PermissionCode) .Distinct() .ToListAsync(); return new { LocalUserId = localUser.Id, Roles = userRoles, Permissions = userPermissions }; }); if (userPermData == null) { context.Fail("当前用户未获得本应用访问权限"); return; } // 追加应用本地身份声明 后续授权逻辑直接用原生API即可 var localIdentity = new ClaimsIdentity(); localIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPermData.LocalUserId.ToString())); foreach (var role in userPermData.Roles) { localIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } foreach (var perm in userPermData.Permissions) { localIdentity.AddClaim(new Claim("app_permission", perm)); } context.Principal.AddIdentity(localIdentity); } }; });
常见避坑点
- 不要将各应用的本地角色、权限回写到IdentityServer4统一签发:这会导致Token体积无限膨胀,且违背各应用独立维护权限体系的要求,权限变更需要跨系统修改统一身份源,维护成本极高。
- 不要给缓存设置过长的过期时间:缓存有效期最多不要超过Access Token有效期的2倍,避免权限变更后长期不生效。
- 不要在业务接口中重复写用户映射逻辑:所有映射逻辑统一在认证管道的
OnTokenValidated事件中完成,业务接口直接通过User对象获取用户信息即可,避免逻辑分散带来的安全漏洞。 - 不要关闭JWT默认的校验逻辑:不要为了调试方便关闭签名校验、过期校验,所有本地权限逻辑的前提是JWT本身是合法可信的。
内容的提问来源于stack exchange,提问作者Craig Savage
相关产品推荐
相关产品推荐

