ASP.NET Core 5 JwtBearer基于用户的动态密钥实现与UserManager获取问题
你目前的实现思路是对的,只是在服务获取的方式上可以更符合ASP.NET Core的依赖注入规范,避免全局变量的使用,同时优化代码结构。
先解释你之前踩坑的根本原因:
- 第一种方案在Startup构造函数注入
UserManager失败,是因为Startup构造函数只能注入主机初始化阶段就注册的服务(比如IConfiguration、IWebHostEnvironment),Identity相关服务是在ConfigureServices执行过程中才注册的,此时容器还未构建完成,自然无法解析。 - 第二种方案调用
BuildServiceProvider会生成一个独立的服务容器实例,导致单例服务被重复创建,会引发服务状态不一致、内存泄漏等问题,这个警告必须处理。 - 你现在在用的第三种方案功能没问题,但全局存储
IApplicationBuilder属于野路子实现,不符合DI的最佳实践,后续维护容易出问题。
推荐优化方案
我们可以用IConfigureNamedOptions<JwtBearerOptions>把JWT配置逻辑抽成单独的服务,所有依赖通过注入获取,完全不需要全局变量:
首先创建JWT选项配置类:
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; public class ConfigureJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions> { private readonly IConfiguration _configuration; private readonly IServiceScopeFactory _scopeFactory; // 直接注入需要的依赖,不需要碰Startup的全局变量 public ConfigureJwtBearerOptions(IConfiguration configuration, IServiceScopeFactory scopeFactory) { _configuration = configuration; _scopeFactory = scopeFactory; } public void Configure(string name, JwtBearerOptions options) { // 只处理JwtBearer默认方案的配置 if (name != JwtBearerDefaults.AuthenticationScheme) return; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidIssuer = _configuration["JWT:Issuer"], ValidAudience = _configuration["JWT:Audience"], IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { var keys = new List<SecurityKey>(); if (securityToken is not JwtSecurityToken jwtToken) return keys; // 建议用用户ID(ClaimTypes.NameIdentifier)查询,比用户名效率高,也避免用户名修改导致的问题 var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); if (userIdClaim == null) return keys; // 用注入的ScopeFactory创建作用域,获取UserManager using var scope = _scopeFactory.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>(); // 加ConfigureAwait(false)减少同步等待的死锁风险 var user = userManager.FindByIdAsync(userIdClaim.Value).ConfigureAwait(false).GetAwaiter().GetResult(); if (user != null) { // 注意用户的JwtSecret长度要至少16字符,否则SymmetricSecurityKey会抛异常 keys.Add(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(user.JwtSecret))); } return keys; }, ClockSkew = TimeSpan.Zero }; } public void Configure(JwtBearerOptions options) => Configure(JwtBearerDefaults.AuthenticationScheme, options); }
然后修改Startup里的服务注册逻辑,非常简洁:
public void ConfigureServices(IServiceCollection services) { // 你的Identity注册逻辑(保持原来的就行) services.AddDefaultIdentity<IdentityUser>(/* 你的Identity配置 */) .AddEntityFrameworkStores<YourAppDbContext>(); // 替换成你项目的DbContext类 // 注册JWT认证,不需要写配置逻辑 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(); // 注册我们自定义的JWT配置类 services.ConfigureOptions<ConfigureJwtBearerOptions>(); // 其他服务注册... }
其他优化建议
- 可以加一层缓存:每次验证令牌都查数据库会有性能损耗,建议用
IMemoryCache把用户的JwtSecret缓存10-15分钟,缓存key用用户ID,用户修改密钥的时候删除对应缓存即可,能大幅降低数据库压力。 - 如果你用的是ASP.NET Core 7及以上版本,可以考虑用自定义的
ISecurityTokenValidator实现异步验证逻辑,避免同步调用异步方法的潜在死锁风险。 - 生成用户密钥的时候建议用随机字符串生成器,确保每个用户的密钥足够复杂,不要用简单的规则生成,避免被破解。
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

