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

.NET Core 6 Web API邮件确认Token重复编码问题及重发功能困惑

问题:.NET Core 6 Web API邮件确认Token存储与回调URL中Token不一致的问题

我正在开发.NET Core 6 Web API的注册功能,已实现邮件确认且功能正常,但作为C#和.NET Core新手,想实现邮件重发功能(用户未收到邮件或未及时确认时),因此创建了ConfirmationToken实体来存储UserManager.GenerateEmailConfirmationTokenAsync()生成的确认Token:

public class ConfirmationToken {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string Token { get; set; }
        public string? ResendToken { get; set; }
        public bool? isUsed { get; set; }
        public DateTime? CreatedAt { get; set; } = DateTime.Now;
        public DateTime? ExpiredAt { get; set; } = DateTime.Now.AddHours(24);
        public virtual User? User { get; set; }
        public string UserId { get; set; }
    }

该实体可存储.NET生成的Token及关联用户ID,但存在Token存储问题:调用GenerateEmailConfirmationTokenAsync()生成的Token经URL编码后存入数据库,示例如下:

CfDJ8MhQpKYkLedOlUKK9%2bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%2fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%2bKRBtt2vlzayqxNkZbl43thYM%2fuzn8oCwkD8fc%2fhByV0wFCSgkUQKUHv3FKf5n%2bNbG3%2b%2bpwczzsooqoGvuyUSjsvqA%3d%3d

但调用确认端点时,回调URL中的Token与数据库存储的不一致,示例URL:

https://localhost:7050/api/Auth/ConfirmEmail?userId=cc985a22-53dd-4e69-a0fe-70b82b6a9925&confirmationToken=CfDJ8MhQpKYkLedOlUKK9%252bU10PcPTorIiCUSmUQ9vyl48rr91cCZnkYEuKKZLf4rLt90Dag6OWpkfbjwyhV1FbGNGuUZ90pUL7vi%252fe1T0JaXoV8SSiyZx41lWHwkb71rLTl3xYaDd6Bq6MaFClUjAEfyoorXZZ3K9ddmKb6Byf28%252bKRBtt2vlzayqxNkZbl43thYM%252fuzn8oCwkD8fc%252fhByV0wFCSgkUQKUHv3FKf5n%252bNbG3%252b%252bpwczzsooqoGvuyUSjsvqA%253d%253d

这导致无法通过Token值从数据库查询对应记录,虽然邮件确认功能正常,但添加重发邮件功能(生成新Token并验证旧Token无效)时遇到阻碍,无法理解Token不一致的原因。相关代码如下:

RegisterUser() - 服务方法
注册用户账户并生成确认链接

public async Task<User> RegisterUser(UserRegisterRequest request) {
            var userNameExists = await _userManager.FindByNameAsync(request.UserName);
            var emailExists = await _userManager.FindByEmailAsync(request.EmailAddress);

            var user = new User() {
                UserName = request.UserName,
                Email = request.EmailAddress,
                //EmailConfirmed = false
            };

            var result = await _userManager.CreateAsync(user, request.Password);

            if (result.Succeeded) {
                var confirmationToken = await _confirmationTokenService.GenerateConfirmationToken(user.Id);

               // 指向确认邮件的端点,传递userId和token
                var callback_url = "https://localhost:7050" + _urlHelper.Action("ConfirmEmail", "Auth",
                 new {
                     userId = confirmationToken.UserId,
                     confirmationToken = confirmationToken.Token
                 });

                return user;
            }

            return null;
        }

GenerateConfirmationToken(string userId) - 服务方法
生成邮件Token并存入数据库及关联用户ID

public async Task<ConfirmationToken> GenerateConfirmationToken(string userId) {
            var user = await _userManager.FindByIdAsync(userId);

            var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);

            var encodedToken = HttpUtility.UrlEncode(token);

            var emailConfirmationToken = new ConfirmationToken {
                Token = encodedToken,
                UserId = user.Id,
                isUsed = true
            };

        // 将编码后的Token及关联用户ID存入数据库表
            await _unitOfWork.ConfirmationTokens.Add(emailConfirmationToken);
            int result = _unitOfWork.Save();
        
            return emailConfirmationToken;
        }

ConfirmToken(string userId, string token) - 服务方法
确认传递的解码后邮件Token和用户ID,供ConfirmEmail端点调用

public async Task<bool> ConfirmToken(string userId, string token) {
            var user = await _userManager.FindByIdAsync(userId);
            var decodedToken = HttpUtility.UrlDecode(token);
            var result = await _userManager.ConfirmEmailAsync(user, decodedToken);

            return result.Succeeded;
   }

问题原因

核心问题是双重URL编码:

  1. 在GenerateConfirmationToken方法中,手动对Token执行了HttpUtility.UrlEncode(token),并将编码后的Token存入数据库。
  2. 生成回调URL时,_urlHelper.Action方法会自动对所有参数进行URL编码,导致已经编码过的Token被再次编码(例如%2b被转成%252b),最终URL中的Token和数据库存储的编码后Token不一致。

修复方案

方案1:存储原始Token,仅在生成URL时自动编码

修改GenerateConfirmationToken方法,去掉手动URL编码,直接存储UserManager生成的原始Token:

public async Task<ConfirmationToken> GenerateConfirmationToken(string userId) {
    var user = await _userManager.FindByIdAsync(userId);
    var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
    // 移除手动URL编码,存储原始Token
    var emailConfirmationToken = new ConfirmationToken {
        Token = token,
        UserId = user.Id,
        isUsed = false // 初始状态应为未使用
    };
    await _unitOfWork.ConfirmationTokens.Add(emailConfirmationToken);
    int result = _unitOfWork.Save();
    return emailConfirmationToken;
}

生成回调URL时,直接传递原始Token,_urlHelper.Action会自动完成URL编码:

var callback_url = "https://localhost:7050" + _urlHelper.Action("ConfirmEmail", "Auth",
 new {
     userId = confirmationToken.UserId,
     confirmationToken = confirmationToken.Token
 });

此时数据库存储的是原始Token,确认端点接收到编码后的Token后,通过HttpUtility.UrlDecode(token)解码即可匹配数据库中的原始Token。

方案2:若坚持存储编码后的Token,生成URL时先解码

如果必须保留数据库中存储编码后的Token,生成URL时需要先解码原始编码Token,再让_urlHelper.Action自动编码:

var callback_url = "https://localhost:7050" + _urlHelper.Action("ConfirmEmail", "Auth",
 new {
     userId = confirmationToken.UserId,
     confirmationToken = HttpUtility.UrlDecode(confirmationToken.Token)
 });

重发邮件功能实现建议

实现重发时需处理旧Token失效与新Token生成:

public async Task<bool> ResendConfirmationEmail(string userId) {
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null || user.EmailConfirmed) return false;
    
    // 标记用户所有未过期、未使用的旧Token为已使用
    var oldValidTokens = await _unitOfWork.ConfirmationTokens
        .Where(t => t.UserId == userId && t.isUsed == false && t.ExpiredAt > DateTime.Now)
        .ToListAsync();
    foreach (var token in oldValidTokens) {
        token.isUsed = true;
        _unitOfWork.ConfirmationTokens.Update(token);
    }
    
    // 生成新Token并保存
    var newToken = await GenerateConfirmationToken(userId);
    // 发送包含新Token链接的邮件逻辑...
    
    _unitOfWork.Save();
    return true;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 16:05:23