const CONFIG = { BLOG_SOURCE: "https://source.example.com", TURNSTILE_URL: "https://challenges.cloudflare.com/turnstile/v0/siteverify", TURNSTILE_SITEKEY: "", TURNSTILE_SECRET: "", JWT_SECRET: "", COOKIE_NAME: "", COOKIE_EXPIRE: 3600, };
async function generateToken(payload) { const header = { alg: "HS256", typ: "JWT" }; const encodedHeader = btoa(JSON.stringify(header)).replace(/=/g, ""); const encodedPayload = btoa(JSON.stringify(payload)).replace(/=/g, "");
const data = `${encodedHeader}.${encodedPayload}`; const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(CONFIG.JWT_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"] );
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); const encodedSignature = btoa( String.fromCharCode(...new Uint8Array(signature)) ).replace(/=/g, "");
return `${data}.${encodedSignature}`; }
async function verifyToken(token) { try { const parts = token.split("."); if (parts.length !== 3) return false;
const [header, payload, signature] = parts; const data = `${header}.${payload}`;
const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(CONFIG.JWT_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, encoder.encode(data) );
if (!isValid) return false;
const decodedPayload = JSON.parse( atob(payload + "===".substring(0, (4 - (payload.length % 4)) % 4)) ); return decodedPayload.exp > Math.floor(Date.now() / 1000); } catch (e) { return false; } }
function getVerificationHTML() { return `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>安全验证</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 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; min-height: 350px; text-align: center; box-sizing: border-box; } .title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 24px; } .description { color: #666; margin-bottom: 24px; font-size: 14px; line-height: 1.5; } .verify-button { background: #667eea; color: white; border: none; border-radius: 8px; padding: 12px 24px; font-size: 16px; cursor: pointer; margin-top: 20px; width: 100%; } .verify-button:hover { background: #5a6fd8; } .verify-button:disabled { background: #ccc; cursor: not-allowed; } .turnstile-container { display: flex; justify-content: center; align-items: center; margin: 20px 0; min-height: 65px; width: 100%; } .cf-turnstile { margin: 0 auto; } </style> </head> <body> <div class="container"> <div class="title">安全验证</div> <p class="description">请完成以下验证后继续访问</p> <form method="POST"> <div class="turnstile-container"> <div class="cf-turnstile" data-sitekey="${CONFIG.TURNSTILE_SITEKEY}" data-callback="onSuccess"></div> </div> <button type="submit" class="verify-button" id="btn" disabled>验证并进入</button> </form> </div> <script>function onSuccess(){document.getElementById('btn').disabled=false;}</script> </body> </html>`; }
async function verifyTurnstile(token) { const response = await fetch(CONFIG.TURNSTILE_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `secret=${encodeURIComponent( CONFIG.TURNSTILE_SECRET )}&response=${encodeURIComponent(token)}`, }); const result = await response.json(); return result.success; }
async function isVerified(request) { const cookie = request.headers.get("Cookie") || ""; const match = cookie.match(new RegExp(CONFIG.COOKIE_NAME + "=([^;]+)")); if (!match) return false;
const token = match[1]; return await verifyToken(token); }
async function createSuccessResponse(targetPath) { const payload = { verified: true, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + CONFIG.COOKIE_EXPIRE, };
const token = await generateToken(payload);
return new Response("", { status: 302, headers: { Location: targetPath, "Set-Cookie": `${CONFIG.COOKIE_NAME}=${token}; Path=/; Max-Age=${CONFIG.COOKIE_EXPIRE}; HttpOnly; SameSite=Lax; Secure`, }, }); }
export default { async fetch(request, env, ctx) { const url = new URL(request.url);
if (!(await isVerified(request))) { if (request.method === "POST") { const formData = await request.formData(); const token = formData.get("cf-turnstile-response");
if (await verifyTurnstile(token)) { return await createSuccessResponse(url.pathname); } else { return new Response("验证失败", { status: 403 }); } }
return new Response(getVerificationHTML(), { headers: { "content-type": "text/html; charset=utf-8" }, }); }
const target = CONFIG.BLOG_SOURCE + url.pathname + url.search; return fetch(target, request); }, };
|