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

ASP.NET中基于JWT的userId从数据库动态加载角色实现问询

解决方案

一、动态加载用户角色与额外信息(仅存userId在JWT)

你需要用到IClaimsTransformation接口,它会在JWT验证通过后自动触发,用来扩展或转换用户的Claims,正好适合从数据库拉取角色、用户详情等信息并添加到当前用户身份中。

1. 实现自定义Claims转换器

创建一个类实现IClaimsTransformation,注入你的数据库上下文(比如AppDbContext):

public class CustomClaimsTransformer : IClaimsTransformation
{
    private readonly AppDbContext _dbContext;

    public CustomClaimsTransformer(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // 从现有Claims中获取userId(和你签发JWT时存的ClaimKey一致,比如"userId"或JwtRegisteredClaimNames.Sub)
        var userIdClaim = principal.FindFirst("userId") ?? principal.FindFirst(JwtRegisteredClaimNames.Sub);
        if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId))
        {
            return principal;
        }

        // 从数据库查询用户及关联角色
        var user = await _dbContext.Users
            .Include(u => u.Roles)
            .FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null)
        {
            return principal;
        }

        // 基于原有身份创建新的ClaimsIdentity,添加扩展信息
        var identity = new ClaimsIdentity(principal.Identity);
        foreach (var role in user.Roles)
        {
            identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
        }
        identity.AddClaim(new Claim(ClaimTypes.Name, user.FullName));
        identity.AddClaim(new Claim("Email", user.Email));

        return new ClaimsPrincipal(identity);
    }
}

2. 注册转换器服务

在ConfigureServices中添加以下代码:

// 注册数据库上下文(若未注册)
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(_configuration.GetConnectionString("DefaultConnection")));

// 注册自定义Claims转换器
services.AddScoped<IClaimsTransformation, CustomClaimsTransformer>();

二、验证Token签发时间是否晚于密码修改时间

在JWT验证通过后,通过AddJwtBearer的Events自定义逻辑,对比Token签发时间与用户密码最后修改时间,拒绝过期的旧Token。

1. 配置JWT验证事件

修改你现有的AddJwtBearer配置:

.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true
    };

    // 添加Token验证后的自定义检查逻辑
    options.Events = new JwtBearerEvents
    {
        OnTokenValidated = async context =>
        {
            var dbContext = context.HttpContext.RequestServices.GetRequiredService<AppDbContext>();
            
            // 获取Token中的userId
            var userIdClaim = context.Principal.FindFirst("userId") ?? context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
            if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId))
            {
                context.Fail("Invalid user ID in token");
                return;
            }

            // 查询用户的密码最后修改时间
            var user = await dbContext.Users.FindAsync(userId);
            if (user == null)
            {
                context.Fail("User not found");
                return;
            }

            // 解析Token的签发时间(iat声明)
            if (!long.TryParse(context.Principal.FindFirst(JwtRegisteredClaimNames.Iat)?.Value, out var issuedTimestamp))
            {
                context.Fail("Invalid issued time in token");
                return;
            }
            var issuedTime = DateTimeOffset.FromUnixTimeSeconds(issuedTimestamp).UtcDateTime;

            // 对比时间,拒绝密码修改前签发的Token
            if (user.PasswordLastModifiedTime > issuedTime)
            {
                context.Fail("Token was issued before password was changed. Please re-login.");
            }
        }
    };
});

2. 签发JWT时必须包含iat声明

生成Token时务必添加签发时间声明,否则上述验证会失败:

// 示例:生成JWT的核心代码
var claims = new[]
{
    new Claim("userId", user.Id.ToString()),
    new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
    // 其他必要声明...
};

var token = new JwtSecurityToken(
    claims: claims,
    expires: DateTime.UtcNow.AddHours(1),
    signingCredentials: new SigningCredentials(new SymmetricSecurityKey(jwtKey), SecurityAlgorithms.HmacSha256Signature)
);

三、控制器中访问扩展后的用户信息

完成配置后,在控制器里直接通过User对象获取扩展后的Claims:

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        var userId = User.FindFirst("userId")?.Value;
        var fullName = User.FindFirst(ClaimTypes.Name)?.Value;
        var email = User.FindFirst("Email")?.Value;
        var roles = User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();

        return Ok(new
        {
            UserId = userId,
            FullName = fullName,
            Email = email,
            Roles = roles
        });
    }
}

内容的提问来源于stack exchange,提问作者Levy Barbosa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 10:15:35