自定义有效期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
相关产品推荐
相关产品推荐

