防护一次性链接:规避邮件服务商钓鱼过滤器的令牌消耗问题
解决方案
以下是几个无需用户额外操作即可区分真人访问与邮件预加载请求的方案,适配你的HTML+TypeScript前端和C#后端技术栈:
1. 改用POST请求触发核心操作
邮件中的按钮先指向一个静态中转页面,页面加载后自动通过POST请求携带令牌执行操作。邮件客户端的预加载只会发起GET请求,不会触发POST,从根源避免误操作。
前端(TypeScript)示例:
// 中转页面加载时自动发起POST请求 window.addEventListener('load', async () => { // 从URL获取token参数 const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); if (token) { try { await fetch('/api/ExecuteDatabaseAction', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: token }) }); // 操作完成后跳转到结果页 window.location.href = '/action-success'; } catch (error) { window.location.href = '/action-failed'; } } });
后端(C#)示例:
[HttpPost("ExecuteDatabaseAction")] public IActionResult ExecuteDatabaseAction([FromBody] TokenRequest request) { // 验证token有效性 if (!ValidateToken(request.Token)) { return BadRequest("无效令牌"); } // 执行数据库操作 PerformDatabaseOperation(); // 标记令牌已使用(根据你的令牌机制调整) InvalidateToken(request.Token); return Ok(); } public class TokenRequest { public string Token { get; set; } }
2. 通过请求头识别预加载请求
邮件客户端的预加载请求通常会携带特定请求头,后端可以先检查这些头,判断是预加载则不执行操作也不消耗令牌。常见标识包括:
Purpose: prefetch(部分浏览器/客户端的预加载头)X-Microsoft-Antispam(Outlook相关标识头)User-Agent中包含bot、preview、crawler等关键词
后端(C#)验证逻辑示例:
[HttpGet("ActionPage")] public IActionResult ActionPage(string token) { // 检查是否为预加载请求 var purposeHeader = Request.Headers["Purpose"].ToString(); var userAgent = Request.Headers["User-Agent"].ToString(); var isPrefetch = purposeHeader.Equals("prefetch", StringComparison.OrdinalIgnoreCase) || userAgent.Contains("bot", StringComparison.OrdinalIgnoreCase) || userAgent.Contains("preview", StringComparison.OrdinalIgnoreCase); if (isPrefetch) { // 仅验证令牌有效性,不执行操作、不消耗令牌 return ValidateToken(token) ? Ok("预加载验证通过") : BadRequest("无效令牌"); } // 真人访问流程:验证令牌+执行操作 if (!ValidateToken(token)) { return BadRequest("无效令牌"); } PerformDatabaseOperation(); InvalidateToken(token); return RedirectToAction("SuccessPage"); }
注意:该方案依赖客户端的请求头标识,可能存在遗漏情况,建议结合其他方案使用。
3. 为令牌设置生效延迟
生成令牌时,同时设置一个短时间的生效延迟(比如10秒)。邮件预加载通常会在邮件到达后几秒内触发,而真人点击一般会在延迟之后。后端验证时,若当前时间未到令牌生效时间,仅验证有效性但不执行操作、不消耗令牌;超过延迟时间后再正常执行。
后端(C#)关键逻辑示例:
// 生成令牌时记录创建时间与生效时间 public string GenerateOneTimeToken() { var token = Guid.NewGuid().ToString(); // 存储令牌信息(示例用Redis,可替换为你的存储方案) _redisCache.SetString(token, JsonConvert.SerializeObject(new { ExpiresAt = DateTime.UtcNow.AddMinutes(5), ActivatesAt = DateTime.UtcNow.AddSeconds(10) // 10秒后生效 })); return token; } // 验证令牌并检查是否已生效 public bool ValidateTokenAndCheckActivation(string token, out bool isActivated) { var tokenData = _redisCache.GetString(token); if (string.IsNullOrEmpty(tokenData)) { isActivated = false; return false; } var data = JsonConvert.DeserializeObject<TokenData>(tokenData); if (DateTime.UtcNow > data.ExpiresAt) { isActivated = false; return false; } isActivated = DateTime.UtcNow >= data.ActivatesAt; return true; } // 接口中使用逻辑 [HttpGet("ActionPage")] public IActionResult ActionPage(string token) { if (!ValidateTokenAndCheckActivation(token, out bool isActivated)) { return BadRequest("无效令牌"); } if (!isActivated) { return Ok("操作将在几秒后生效,请稍候"); } // 执行数据库操作 PerformDatabaseOperation(); InvalidateToken(token); return RedirectToAction("SuccessPage"); } public class TokenData { public DateTime ExpiresAt { get; set; } public DateTime ActivatesAt { get; set; } }
4. Cookie+令牌双重验证
邮件链接携带令牌作为GET参数,用户点击进入页面后,后端先验证令牌有效性,然后设置一个HttpOnly的Cookie;接着前端自动发起后续请求(页面内AJAX),同时携带令牌和Cookie。预加载请求不会保存Cookie,因此只有真人访问的后续请求才会同时具备有效令牌和Cookie,后端仅在两者都验证通过时执行操作。
前端(TypeScript)示例:
window.addEventListener('load', async () => { const token = new URLSearchParams(window.location.search).get('token'); if (token) { // 先请求验证令牌并设置Cookie await fetch(`/api/ValidateTokenAndSetCookie?token=${token}`); // 发起操作请求(携带Cookie) await fetch('/api/ExecuteDatabaseAction', { method: 'POST', credentials: 'include' }); window.location.href = '/action-success'; } });
后端(C#)示例:
[HttpGet("ValidateTokenAndSetCookie")] public IActionResult ValidateTokenAndSetCookie(string token) { if (!ValidateToken(token)) { return BadRequest("无效令牌"); } // 设置HttpOnly安全Cookie Response.Cookies.Append("action_token", token, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTime.UtcNow.AddMinutes(5) }); return Ok(); } [HttpPost("ExecuteDatabaseAction")] public IActionResult ExecuteDatabaseAction() { var tokenFromCookie = Request.Cookies["action_token"]; var tokenFromQuery = Request.Query["token"].ToString(); if (string.IsNullOrEmpty(tokenFromCookie) || tokenFromCookie != tokenFromQuery || !ValidateToken(tokenFromCookie)) { return BadRequest("验证失败"); } PerformDatabaseOperation(); InvalidateToken(tokenFromCookie); // 操作完成后清除Cookie Response.Cookies.Delete("action_token"); return Ok(); }
内容的提问来源于stack exchange,提问作者JanLaunh
相关产品推荐
相关产品推荐

