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

关于ASP.NET Core Identity双因素令牌使用后仍有效及限制其单次使用的技术问询

ASP.NET Core Identity双因素令牌使用后仍有效及限制其单次使用的技术问询

嘿,这个问题提得很实在!确实,ASP.NET Core Identity默认生成的双因素令牌(就像你用await _userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);生成的邮件令牌)是时间驱动型的,默认有效期一般是5分钟左右,而且在有效期内可以重复使用——这确实存在安全风险,要是令牌不小心泄露,别人在有效期内就能多次用它登录。

那能不能改成用一次就失效呢?答案是肯定的,但默认Identity并没有内置这个功能,得咱们自己扩展一下逻辑。下面给你两个可行的实现方案:

方案一:自定义令牌提供器(TokenProvider)

你可以实现一个自定义的IUserTwoFactorTokenProvider<TUser>,核心思路是:生成令牌时把它和用户ID、使用状态、过期时间存在数据库里(比如新增一张TwoFactorTokenHistory表),验证时先查令牌是否过期、是否未被使用,验证通过后立刻标记为已使用。

给你个简化版的代码参考:

public class SingleUseEmailTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : class
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IOptions<DataProtectionTokenProviderOptions> _tokenOptions;
    private readonly IDataProtector _dataProtector;

    public SingleUseEmailTokenProvider(ApplicationDbContext dbContext, 
                                       IOptions<DataProtectionTokenProviderOptions> tokenOptions,
                                       IDataProtectionProvider dataProtectionProvider)
    {
        _dbContext = dbContext;
        _tokenOptions = tokenOptions;
        _dataProtector = dataProtectionProvider.CreateProtector("SingleUseEmailTokenProtection");
    }

    public async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
    {
        var userId = await manager.GetUserIdAsync(user);
        // 生成随机令牌
        var rawToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
        var expiresAt = DateTime.UtcNow.Add(_tokenOptions.Value.TokenLifespan);

        // 加密后存入数据库
        _dbContext.TwoFactorTokenHistories.Add(new TwoFactorTokenHistory
        {
            UserId = userId,
            Token = _dataProtector.Protect(rawToken),
            Purpose = purpose,
            IsUsed = false,
            ExpiresAt = expiresAt
        });
        await _dbContext.SaveChangesAsync();

        return rawToken;
    }

    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
    {
        var userId = await manager.GetUserIdAsync(user);
        var protectedToken = _dataProtector.Protect(token);
        // 查询有效未使用的令牌
        var tokenRecord = await _dbContext.TwoFactorTokenHistories
            .FirstOrDefaultAsync(t => t.UserId == userId 
                                      && t.Purpose == purpose 
                                      && t.Token == protectedToken 
                                      && !t.IsUsed 
                                      && t.ExpiresAt > DateTime.UtcNow);

        if (tokenRecord == null)
            return false;

        // 标记为已使用
        tokenRecord.IsUsed = true;
        await _dbContext.SaveChangesAsync();

        return true;
    }

    // 实现接口要求的其他方法
    public Task<bool> CanGenerateAsync(UserManager<TUser> manager, TUser user)
    {
        return Task.FromResult(true);
    }
}

然后在Program.cs(或Startup.cs)里注册这个自定义提供器:

builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddTokenProvider<SingleUseEmailTokenProvider<ApplicationUser>>("SingleUseEmail");

之后生成令牌时就用这个自定义的提供器名称:

await _userManager.GenerateTwoFactorTokenAsync(user, "SingleUseEmail");

方案二:复用Identity自带的AspNetUserTokens表

Identity本身有一张AspNetUserTokens表,专门用来存储用户的各类令牌。你可以利用这张表,在生成令牌后把它存进去,验证时先查是否存在且未被使用,验证通过后立刻删除。

生成令牌的示例代码:

// 生成默认时间令牌
var token = await _userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
// 存入AspNetUserTokens表,标记为单次使用的邮件令牌
await _userManager.SetAuthenticationTokenAsync(user, "SingleUseTwoFactor", "EmailToken", token);

验证令牌的逻辑:

// 取出存储的令牌
var storedToken = await _userManager.GetAuthenticationTokenAsync(user, "SingleUseTwoFactor", "EmailToken");
if (storedToken == null || storedToken != token)
{
    return false;
}
// 先验证原令牌是否在有效期内
var isTokenValid = await _userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider, token);
if (isTokenValid)
{
    // 删除令牌,确保无法再次使用
    await _userManager.RemoveAuthenticationTokenAsync(user, "SingleUseTwoFactor", "EmailToken");
    return true;
}
return false;

这个方案的优点是不用额外建表,缺点是如果用户短时间内生成多个令牌,你需要定时清理过期的旧令牌,避免表数据冗余。

额外提示

  • 不管用哪个方案,都要记得清理过期的令牌数据,比如加个定时任务,每天删除过期且未使用的令牌。
  • 存储令牌时尽量加密,避免明文存储带来的安全风险,方案一里用的IDataProtector就是个不错的选择。

总的来说,默认的时间令牌不支持单次使用,但通过扩展Identity的令牌逻辑,完全能实现这个需求,你可以根据自己项目的实际情况选合适的方案~

备注:内容来源于stack exchange,提问作者roczstar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 14:24:32