.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
相关产品推荐
相关产品推荐

