You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:03:19