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

.NET 8 Microsoft Identity登录及刷新时存取令牌到数据库的问题

.NET 8 Microsoft Identity令牌数据库管控实现方案

核心问题:手动获取登录/刷新时的令牌

默认的PasswordSignInAsync不会暴露生成的令牌内容,因此需要放弃默认登录逻辑,手动调用Identity的令牌生成器,直接获取访问令牌和刷新令牌后存入数据库。

1. 定义令牌存储实体

先创建数据库实体,用于记录令牌生命周期:

public class UserTokenRecord
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string UserId { get; set; }
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
    public DateTime AccessTokenExpiresAt { get; set; }
    public DateTime RefreshTokenExpiresAt { get; set; }
    public bool IsRevoked { get; set; } = false;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    // 关联用户表
    public virtual ApplicationUser User { get; set; }
}

在DbContext中添加DbSet<UserTokenRecord>并配置EF Core映射。

2. 自定义登录端点(替代默认/login)

注入UserManager<ApplicationUser>和ITokenProvider<ApplicationUser>,手动完成密码验证、令牌生成、数据库存储:

app.MapPost("/api/auth/login", async (
    LoginRequest request,
    UserManager<ApplicationUser> userManager,
    ITokenProvider<ApplicationUser> tokenProvider,
    AppDbContext dbContext) =>
{
    var user = await userManager.FindByNameAsync(request.Username);
    if (user == null || !await userManager.CheckPasswordAsync(user, request.Password))
    {
        return Results.Unauthorized();
    }

    // 1. 吊销该用户所有未过期、未失效的旧令牌
    var activeTokens = await dbContext.UserTokenRecords
        .Where(t => t.UserId == user.Id && !t.IsRevoked && t.AccessTokenExpiresAt > DateTime.UtcNow)
        .ToListAsync();
    foreach (var token in activeTokens)
    {
        token.IsRevoked = true;
    }
    await dbContext.SaveChangesAsync();

    // 2. 生成新令牌
    var accessTokenResult = await tokenProvider.GenerateAccessTokenAsync(user, cancellationToken: default);
    var refreshTokenResult = await tokenProvider.GenerateRefreshTokenAsync(user, cancellationToken: default);

    // 3. 存入数据库
    var newTokenRecord = new UserTokenRecord
    {
        UserId = user.Id,
        AccessToken = accessTokenResult.Value,
        RefreshToken = refreshTokenResult.Value,
        AccessTokenExpiresAt = accessTokenResult.Expires,
        RefreshTokenExpiresAt = refreshTokenResult.Expires
    };
    dbContext.UserTokenRecords.Add(newTokenRecord);
    await dbContext.SaveChangesAsync();

    // 返回令牌给客户端
    return Results.Ok(new
    {
        AccessToken = accessTokenResult.Value,
        RefreshToken = refreshTokenResult.Value,
        ExpiresAt = accessTokenResult.Expires
    });
})
.WithName("Login")
.WithOpenApi();

其中LoginRequest是简单的DTO:

public class LoginRequest
{
    public string Username { get; set; }
    public string Password { get; set; }
}

3. 自定义刷新令牌端点(替代默认/refresh)

实现刷新时的数据库校验、旧令牌吊销、新令牌生成:

app.MapPost("/api/auth/refresh", async (
    RefreshRequest request,
    UserManager<ApplicationUser> userManager,
    ITokenProvider<ApplicationUser> tokenProvider,
    AppDbContext dbContext) =>
{
    // 1. 从数据库查询刷新令牌,校验有效性
    var tokenRecord = await dbContext.UserTokenRecords
        .FirstOrDefaultAsync(t => t.RefreshToken == request.RefreshToken 
                                  && !t.IsRevoked 
                                  && t.RefreshTokenExpiresAt > DateTime.UtcNow);
    if (tokenRecord == null)
    {
        return Results.Unauthorized();
    }

    var user = await userManager.FindByIdAsync(tokenRecord.UserId);
    if (user == null)
    {
        return Results.Unauthorized();
    }

    // 2. 吊销旧令牌
    tokenRecord.IsRevoked = true;
    await dbContext.SaveChangesAsync();

    // 3. 生成新令牌并存储
    var accessTokenResult = await tokenProvider.GenerateAccessTokenAsync(user, cancellationToken: default);
    var refreshTokenResult = await tokenProvider.GenerateRefreshTokenAsync(user, cancellationToken: default);

    var newTokenRecord = new UserTokenRecord
    {
        UserId = user.Id,
        AccessToken = accessTokenResult.Value,
        RefreshToken = refreshTokenResult.Value,
        AccessTokenExpiresAt = accessTokenResult.Expires,
        RefreshTokenExpiresAt = refreshTokenResult.Expires
    };
    dbContext.UserTokenRecords.Add(newTokenRecord);
    await dbContext.SaveChangesAsync();

    return Results.Ok(new
    {
        AccessToken = accessTokenResult.Value,
        RefreshToken = refreshTokenResult.Value,
        ExpiresAt = accessTokenResult.Expires
    });
})
.WithName("RefreshToken")
.WithOpenApi();

RefreshRequest DTO:

public class RefreshRequest
{
    public string RefreshToken { get; set; }
}

4. 全局校验Bearer令牌有效性

自定义一个授权处理程序,每次API请求时校验数据库中的令牌状态:

public class TokenValidationHandler : AuthorizationHandler<TokenValidationRequirement>
{
    private readonly AppDbContext _dbContext;

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

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        TokenValidationRequirement requirement)
    {
        // 提取Bearer令牌
        if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader) 
            || !authHeader.ToString().StartsWith("Bearer "))
        {
            context.Fail();
            return;
        }

        var accessToken = authHeader.ToString().Substring("Bearer ".Length);

        // 查询数据库校验令牌
        var tokenRecord = await _dbContext.UserTokenRecords
            .FirstOrDefaultAsync(t => t.AccessToken == accessToken 
                                      && !t.IsRevoked 
                                      && t.AccessTokenExpiresAt > DateTime.UtcNow);
        if (tokenRecord != null)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}

// 定义授权需求
public class TokenValidationRequirement : IAuthorizationRequirement { }

然后在Program.cs中注册:

builder.Services.AddScoped<IAuthorizationHandler, TokenValidationHandler>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ValidToken", policy =>
        policy.Requirements.Add(new TokenValidationRequirement()));
});

之后在需要校验的API端点上添加[Authorize(Policy = "ValidToken")]即可。

关键注意事项

  • 令牌存储安全性:AccessToken和RefreshToken属于敏感数据,建议使用ASP.NET Core的IDataProtector加密后再存入数据库。
  • 数据清理:定期清理数据库中过期或已吊销的令牌记录,避免数据冗余。
  • 性能优化:可结合分布式缓存缓存有效令牌,但需保证缓存与数据库状态实时同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 07:17:07