const DEFAULT_CONFIG = { TURNSTILE_URL: "https://challenges.cloudflare.com/turnstile/v0/siteverify", TURNSTILE_EXPIRE: 3600, JWT_ALGORITHM: "HS256", };
async function generateToken(payload, secret) { const header = btoa( JSON.stringify({ alg: DEFAULT_CONFIG.JWT_ALGORITHM, typ: "JWT" }) ).replace(/=/g, ""); const encodedPayload = btoa(JSON.stringify(payload)).replace(/=/g, ""); const data = `${header}.${encodedPayload}`;
const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] );
const signature = await crypto.subtle.sign( "HMAC", key, new TextEncoder().encode(data) ); return `${data}.${btoa( String.fromCharCode(...new Uint8Array(signature)) ).replace(/=/g, "")}`; }
async function verifyToken(token, secret) { console.log(`JWT Token 获取: ${token}`); try { const [header, payload, signature] = token.split("."); if (!header || !payload || !signature) return false;
const data = `${header}.${payload}`; const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"] );
const expectedSignature = Uint8Array.from( atob(signature + "===".substring(0, (4 - (signature.length % 4)) % 4)), (c) => c.charCodeAt(0) );
const isValid = await crypto.subtle.verify( "HMAC", key, expectedSignature, new TextEncoder().encode(data) );
console.log(`JWT Token 有效: ${isValid}`); if (!isValid) return false;
const decodedPayload = JSON.parse( atob(payload + "===".substring(0, (4 - (payload.length % 4)) % 4)) ); console.log(`JWT Token 解码: ${JSON.stringify(decodedPayload)}`);
const isExpired = decodedPayload.exp > Math.floor(Date.now() / 1000); console.log(`JWT Token 过期: ${!isExpired}`); return isExpired; } catch (e) { console.log(`JWT Token 失败: ${e}`); return false; } }
async function verifyTurnstile(response, remoteip, secretKey) { console.log(`Turnstile 质询: ${remoteip}`); const formData = new FormData(); formData.append("secret", secretKey); formData.append("response", response); formData.append("remoteip", remoteip);
const result = await ( await fetch(DEFAULT_CONFIG.TURNSTILE_URL, { method: "POST", body: formData, }) ).json();
console.log(`Turnstile 结果: ${JSON.stringify(result)}`); return result.success; }
async function isVerified(request, cookieName, jwtSecret) { const cookie = request.headers.get("Cookie") || ""; const match = cookie.match(new RegExp(`${cookieName}=([^;]+)`)); return match ? await verifyToken(match[1], jwtSecret) : false; }
function getTurnstileHTML({ siteKey, title = "安全验证", logo = "🔒", description = "请完成以下验证后继续使用", }) { return `<!DOCTYPE html> <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <style> body{font-family:system-ui;background:linear-gradient(135deg,#667eea,#764ba2);margin:0;padding:0;min-height:100vh;display:flex;align-items:center;justify-content:center} .container{background:white;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);padding:40px;width:400px;text-align:center} .logo{font-size:24px;font-weight:bold;color:#333;margin-bottom:24px} .title{color:#333;margin-bottom:16px;font-size:18px} .description{color:#666;margin-bottom:24px;font-size:14px} .turnstile-container{display:flex;justify-content:center;margin:20px 0;min-height:65px} </style> </head><body><div class="container"><div class="logo">${logo}</div><h2 class="title">${title}</h2><p class="description">${description}</p> <form method="POST"><div class="turnstile-container"><div class="cf-turnstile" data-sitekey="${siteKey}" data-callback="onSuccess"></div></div></form></div> <script>function onSuccess(){document.querySelector('form').submit();}</script></body></html>`; }
async function setSuccessResponse( targetPath, cookieName, jwtSecret, expireTime ) { const payload = { verified: true, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + expireTime, };
const token = await generateToken(payload, jwtSecret); console.log(`Cookie 设置: ${token}`);
return new Response("", { status: 302, headers: { Location: targetPath, "Set-Cookie": `${cookieName}=${token}; Path=/; Max-Age=${expireTime}; HttpOnly; SameSite=Lax; Secure`, }, }); }
export function turnstileMiddleware(options = {}) { const { cookieName = "app_verified", logo = "🔒", title = "安全验证", description = "请完成以下验证后继续使用", skipPaths = [], skipMethods = ["OPTIONS"], } = options;
return async function turnstileAuth(request, env) { const url = new URL(request.url); const { pathname, search } = url; const { method } = request;
if ( skipMethods.includes(method) || skipPaths.some((path) => pathname.startsWith(path)) ) { return null; }
if (!(await isVerified(request, cookieName, env.JWT_SECRET))) { if (method === "POST") { const formData = await request.formData(); const remoteip = request.headers.get("CF-Connecting-IP"); const response = formData.get("cf-turnstile-response");
if ( await verifyTurnstile(response, remoteip, env.TURNSTILE_SECRET_KEY) ) { const expireTime = parseInt(env.TURNSTILE_EXPIRE) || DEFAULT_CONFIG.TURNSTILE_EXPIRE; return await setSuccessResponse( pathname + search, cookieName, env.JWT_SECRET, expireTime ); } else { return new Response("验证失败", { status: 403 }); } }
console.log("跳转 Turnstile 页面"); return new Response( getTurnstileHTML({ siteKey: env.TURNSTILE_SITE_KEY, title, logo, description, }), { headers: { "content-type": "text/html; charset=utf-8" }, } ); }
return null; }; }
|