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

基于IdentityServer与ASP.NET Core Identity的授权认证分离及JWT优化问询

最优实现方案:避免令牌膨胀,API端运行时校验资源权限

你的思路完全正确——不要将大量细粒度的资源ID塞进JWT令牌,而是在API端结合ASP.NET Core Identity的声明数据,在请求运行时做权限校验。以下是具体的实现方案:

一、为什么不推荐在令牌中塞资源ID?

  • 令牌体积膨胀:JWT是自包含令牌,大量资源ID会增大令牌体积,增加传输带宽消耗和解析耗时。
  • 权限更新不及时:令牌一旦签发就无法修改,若用户的资源权限变更,必须重新登录才能获取新令牌,灵活性极差。
  • 安全风险:过多敏感资源ID暴露在令牌中,增加了信息泄露的风险。

二、核心实现思路

  1. 令牌只保留粗粒度身份信息:在ProfileService中仅返回用户ID、角色、API范围等稳定的核心声明,移除store_ids这类细粒度资源ID。
  2. API端运行时查询权限:在API服务中注入ApplicationDbContext或UserManager<IdentityUser>,通过当前用户ID查询其拥有的资源权限,再判断是否允许访问目标资源。
  3. 缓存优化性能:对用户的资源权限做缓存,避免每次请求都查询数据库。

三、具体代码实现

1. 调整ProfileService,精简令牌内容

public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
    // 只返回核心身份标识和粗粒度权限
    var userId = context.Subject.FindFirstValue(ClaimTypes.NameIdentifier);
    if (!string.IsNullOrEmpty(userId))
    {
        context.IssuedClaims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
        // 若需保留角色信息,可从UserManager获取后添加
        // var user = await _userManager.FindByIdAsync(userId);
        // var roles = await _userManager.GetRolesAsync(user);
        // context.IssuedClaims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
    }
    return Task.CompletedTask;
}

2. API端实现自定义资源授权逻辑

方式一:使用ASP.NET Core授权策略(推荐,适合复杂场景)

先定义授权需求:

public class StoreAccessRequirement : IAuthorizationRequirement
{
    // 可根据需要扩展属性,比如资源类型
}

实现授权处理器:

public class StoreAccessHandler : AuthorizationHandler<StoreAccessRequirement>
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IDistributedCache _cache;

    public StoreAccessHandler(UserManager<IdentityUser> userManager, IDistributedCache cache)
    {
        _userManager = userManager;
        _cache = cache;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, StoreAccessRequirement requirement)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userId == null)
        {
            context.Fail();
            return;
        }

        // 从缓存或数据库获取用户的store_ids
        var userStoreIds = await GetUserStoreIdsAsync(userId);
        // 从请求路由中获取目标storeId
        var httpContext = context.Resource as HttpContext;
        var targetStoreId = httpContext?.Request.RouteValues["storeId"]?.ToString();

        if (!string.IsNullOrEmpty(targetStoreId) && userStoreIds.Contains(targetStoreId))
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }

    // 带缓存的权限查询方法
    private async Task<IEnumerable<string>> GetUserStoreIdsAsync(string userId)
    {
        var cacheKey = $"User_StoreIds_{userId}";
        var cachedIds = await _cache.GetStringAsync(cacheKey);
        
        if (!string.IsNullOrEmpty(cachedIds))
        {
            return cachedIds.Split(',');
        }

        var user = await _userManager.FindByIdAsync(userId);
        var storeIds = (await _userManager.GetClaimsAsync(user))
            .Where(c => c.Type == "store_ids")
            .Select(c => c.Value);

        // 缓存15分钟,权限变更时需主动清除缓存
        await _cache.SetStringAsync(cacheKey, string.Join(',', storeIds), new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
        });

        return storeIds;
    }
}

注册授权服务:

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("StoreAccess", policy =>
        policy.Requirements.Add(new StoreAccessRequirement()));
});

builder.Services.AddScoped<IAuthorizationHandler, StoreAccessHandler>();

控制器中使用:

[ApiController]
[Route("api/stores")]
[Authorize(Scope = "api1")]
public class StoresController : ControllerBase
{
    [HttpGet("{storeId}")]
    [Authorize(Policy = "StoreAccess")]
    public async Task<IActionResult> GetStore(string storeId)
    {
        // 业务逻辑
        return Ok(new { StoreId = storeId, Status = "Accessible" });
    }
}

方式二:直接在控制器/服务层校验(适合简单场景)

[ApiController]
[Route("api/stores")]
[Authorize(Scope = "api1")]
public class StoresController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IDistributedCache _cache;

    public StoresController(UserManager<IdentityUser> userManager, IDistributedCache cache)
    {
        _userManager = userManager;
        _cache = cache;
    }

    [HttpGet("{storeId}")]
    public async Task<IActionResult> GetStore(string storeId)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        var userStoreIds = await GetUserStoreIdsAsync(userId);

        if (!userStoreIds.Contains(storeId))
        {
            return Forbid();
        }

        // 业务逻辑
        return Ok(new { StoreId = storeId });
    }

    // 复用上面的GetUserStoreIdsAsync方法
    private async Task<IEnumerable<string>> GetUserStoreIdsAsync(string userId)
    {
        // 缓存+数据库查询逻辑
    }
}

四、补充说明

  • 权限变更处理:当用户的资源权限(如store_ids)发生变化时,需要主动清除对应的缓存,确保下次请求能获取最新权限。
  • 性能平衡:缓存时间需根据业务调整,既不能太短导致频繁查库,也不能太长导致权限更新不及时。

内容的提问来源于stack exchange,提问作者Tyler V

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 20:55:09