登录、注册、找回密码、支付确认等接口是恶意流量的重点攻击入口。验证机制的核心目标不是"让用户更难操作",而是"区分真实用户与自动化脚本"。本文系统对比短信验证码、图形验证码、行为验证三类方案,详解 Cloudflare Turnstile 的接入流程与选型策略。
核心原则:前端只负责收集验证结果,最终判断权必须在后端。
interface LoginPayload {
username: string;
password: string;
captchaToken?: string;
}
async function submitLogin(payload: LoginPayload) {
return fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}防护要点:
function collectClientSignals() {
return {
userAgent: navigator.userAgent,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${window.screen.width}x${window.screen.height}`,
};
}Turnstile 是替代传统 CAPTCHA 的验证方案,通过浏览器环境与风险信号完成验证,必要时才触发轻量交互。可独立使用,不要求站点托管在 Cloudflare 上。
三种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Managed(推荐) | 根据风险动态决定是否需要交互 | 通用场景 |
| Non-Interactive | 用户可见但尽量无交互 | 需要视觉反馈 |
| Invisible | 完全后台验证 | 极致体验 |
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async defer
></script>
<form id="login-form">
<input name="username" />
<input name="password" type="password" />
<div class="cf-turnstile" data-sitekey="你的站点公钥"></div>
<button type="submit">登录</button>
</form>async function validateTurnstileToken(token: string, remoteip?: string) {
const params = new URLSearchParams();
params.append("secret", process.env.TURNSTILE_SECRET_KEY || "");
params.append("response", token);
if (remoteip) params.append("remoteip", remoteip);
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body: params }
);
return response.json();
}关键约束:
cf_clearance 与预清除标准 Turnstile 返回一次性 token。只有启用 pre-clearance 时,才会签发 cf_clearance Cookie 用于后续请求减少重复挑战。这属于 Cloudflare WAF 体系,不应与业务登录态混淆。
| 维度 | 关注点 |
|---|---|
| 成本 | 按次/按峰值/包年包月 |
| 可达性 | 目标用户网络环境能否稳定访问 |
| 用户体验 | 是否需要频繁点图、拖拽 |
| 安全能力 | 风控、风险分级、服务端校验 |
| 集成复杂度 | 前后端接入成本、运维复杂度 |
| 合规要求 | 隐私政策、数据跨境、Cookie 披露 |
function getRiskLevel(action: "login" | "register" | "sms") {
const riskMap = {
login: "medium",
register: "high",
sms: "very-high",
} as const;
return riskMap[action];
}平台通常结合流量模式和行为模式判断:
type VisitEvent = { path: string; timestamp: number };
function isSuspiciousBurst(events: VisitEvent[]) {
if (events.length < 10) return false;
const duration = events[events.length - 1].timestamp - events[0].timestamp;
return duration < 3_000;
}| 问题 | 原因 | 解决方案 |
|---|---|---|
| 前端验证成功但后端拦截 | 未调用服务端 Siteverify API | 后端必须二次校验 token |
| token 经常失效 | 有效期 5 分钟且单次使用 | 超时后重新拉起验证,不复用历史 token |
| 短信接口被刷爆 | 无前置风险控制 | 发短信前加行为验证 + 限流 |
| 海外方案国内不稳定 | 网络可达性差 | 选型时验证真实网络环境,准备降级方案 |
| 风控太严误杀正常用户 | 阈值过激 | 结合日志调参,按风险分级而非一刀切 |