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

自定义有效期6位验证码无密码认证:VerifyUserTokenAsync返回false问题

自定义有效期6位验证码无密码认证验证失败问题解决

我想实现带自定义有效期的6位验证码无密码认证,通过继承DataProtectorTokenProvider完成开发后,已成功修改有效期并生成6位验证码,但调用VerifyUserTokenAsync时始终返回false。


自定义TokenProvider相关代码

public static class CustomIdentityBuilderExtensions
{            
    public static IdentityBuilder AddPasswordlessLoginTokenProvider(this IdentityBuilder builder)
    {
        var userType = builder.UserType;
        var provider = typeof(PasswordlessLoginTokenProvider<>).MakeGenericType(userType);
        return builder.AddTokenProvider("PasswordlessLoginProvider", provider);
    }
}

public class PasswordlessLoginTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public PasswordlessLoginTokenProviderOptions()
    {
        // 更新默认配置
        Name = "PasswordlessLoginTokenProvider";
        TokenLifespan = TimeSpan.FromMinutes(5);
    }
}

public class PasswordlessLoginTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
{
    public PasswordlessLoginTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions<PasswordlessLoginTokenProviderOptions> options, ILogger<DataProtectorTokenProvider<TUser>> logger) 
        : base(dataProtectionProvider, options, logger)
    {
    }

    public override Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
    {
        // 生成6位验证码,但验证不通过
        Random generator = new();
        string token = generator.Next(0, 1000000).ToString("D6");
        return Task.FromResult(token);
    }

    public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
    {
        return Task.FromResult(false);
    }
}

生成与验证代码

生成验证码:

string confirmationToken = await _userManager.GenerateUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth");

验证验证码:

bool isValid = await _userManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token);

备注

曾尝试使用TotpSecurityStampBasedTokenProvider,但它不支持修改验证码有效期。

DataProtectorTokenProvider原生生成逻辑

public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
    if (user == null)
    {
        throw new ArgumentNullException(nameof(user));
    }
    var ms = new MemoryStream();
    var userId = await manager.GetUserIdAsync(user);
    using (var writer = ms.CreateWriter())
    {
        writer.Write(DateTimeOffset.UtcNow);
        writer.Write(userId);
        writer.Write(purpose ?? "");
        string? stamp = null;
        if (manager.SupportsUserSecurityStamp)
        {
            stamp = await manager.GetSecurityStampAsync(user);
        }
        writer.Write(stamp ?? "");
    }
    var protectedBytes = Protector.Protect(ms.ToArray());
    return Convert.ToBase64String(protectedBytes);
}

问题原因

DataProtectorTokenProvider的默认VerifyAsync方法会对token进行解密,提取生成时间、用户ID、认证目的、安全戳等信息验证有效性。重写GenerateAsync后生成的是纯随机6位字符串,不包含这些加密验证信息,导致验证逻辑无法匹配,返回false。

解决方案

思路1:重写VerifyAsync方法,自定义验证逻辑

如果不需要依赖DataProtector的加密验证,可以自行实现验证逻辑,比如将生成的验证码和用户信息缓存起来,验证时对比:

public override async Task<bool> VerifyAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
{
    // 示例逻辑:从缓存中获取用户对应验证码并验证有效期
    var userId = await manager.GetUserIdAsync(user);
    var cachedData = await _cache.GetAsync($"passwordless-code:{userId}:{purpose}");
    if (cachedData == null) return false;
    
    var (storedToken, expirationTime) = JsonSerializer.Deserialize<(string Token, DateTime Expiration)>(cachedData);
    return storedToken == token && expirationTime > DateTime.UtcNow;
}

// 对应GenerateAsync需要修改为存储验证码到缓存
public override async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
    Random generator = new();
    string token = generator.Next(0, 1000000).ToString("D6");
    var userId = await manager.GetUserIdAsync(user);
    var expirationTime = DateTime.UtcNow.Add(Options.TokenLifespan);
    
    await _cache.SetAsync(
        $"passwordless-code:{userId}:{purpose}",
        JsonSerializer.Serialize((token, expirationTime)),
        expirationTime);
    
    return Task.FromResult(token);
}

思路2:基于DataProtector加密6位验证码,保留原生验证逻辑

如果想利用Identity的安全机制(比如用户安全戳变化时失效验证码),可以将6位码和验证所需信息一起加密:

public override async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
    // 生成6位验证码
    Random generator = new();
    string code = generator.Next(0, 1000000).ToString("D6");

    // 结合验证信息加密,保留原生验证逻辑
    var ms = new MemoryStream();
    var userId = await manager.GetUserIdAsync(user);
    using (var writer = ms.CreateWriter())
    {
        writer.Write(DateTimeOffset.UtcNow);
        writer.Write(userId);
        writer.Write(purpose ?? "");
        string? stamp = null;
        if (manager.SupportsUserSecurityStamp)
        {
            stamp = await manager.GetSecurityStampAsync(user);
        }
        writer.Write(stamp ?? "");
        writer.Write(code); // 加入自定义6位验证码
    }
    var protectedBytes = Protector.Protect(ms.ToArray());
    var encryptedToken = Convert.ToBase64String(protectedBytes);
    
    // 将6位码和加密token关联存储,返回6位码给用户
    var userId = await manager.GetUserIdAsync(user);
    await _cache.SetAsync($"passwordless-encrypted:{userId}:{purpose}", encryptedToken, Options.TokenLifespan);
    
    return Task.FromResult(code);
}

// 验证时先通过6位码找到加密token,再调用原生验证逻辑
// 外部验证代码调整:
var encryptedToken = await _cache.GetAsync($"passwordless-encrypted:{user.Id}:passwordless-auth");
bool isValid = await _userManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", encryptedToken);
// 额外对比用户输入的6位码和加密token中提取的码(需重写VerifyAsync提取)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 23:48:15