思路是这样的:
/admin),用一个专门的管理密码保护它。这样,你永远不需要再登录 Cloudflare Dashboard,只需要在浏览器里打开你的管理页面就能修改。而且整个过程依然很安全,管理密码独立于访问密码。
你需要稍微调整一下 Cloudflare 的配置。
MY_CONTENT。CONTENT_KV(必须完全一致),然后选择你刚才创建的命名空间。在同一个 变量 页面,点击 添加环境变量(都设置为密钥)。
| 变量 | 值(都行,自己记住) | 作用 |
|---|---|---|
| SECRET_KEY | 例如:my_token | 用于加密签名 Token,防止伪造,建议32位以上 |
| SECRET_PASSWORD | 例如:my_pwd | 用于验证用户输入的密码 |
| ADMIN_PATH | 例如:/my_dklsjfgeir | 用于进入admin界面(注意:一定要以/开头) |
| ADMIN_PASSWORD | 例如:my_apwd | 用于解锁admin界面 |
把原来 Worker 的代码完全替换成下面这个新代码。
javascript
x// ==============================================// Cloudflare Worker — 动态内容保护 + 在线管理// ==============================================
async function generateToken(secretKey) { const header = { alg: 'HS256', typ: 'JWT' }; const payload = { exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000) }; const encoder = new TextEncoder(); const base64url = (obj) => btoa(JSON.stringify(obj)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const unsignedToken = `${base64url(header)}.${base64url(payload)}`; const key = await crypto.subtle.importKey('raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(unsignedToken)); const sigBase64 = btoa(String.fromCharCode(new Uint8Array(signature))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return `${unsignedToken}.${sigBase64}`;}
async function verifyToken(token, secretKey) { try { const parts = token.split('.'); if (parts.length !== 3) return false; const encoder = new TextEncoder(); const key = await crypto.subtle.importKey('raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']); const unsignedToken = `${parts[0]}.${parts[1]}`; const signature = Uint8Array.from(atob(parts[2].replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); const isValid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(unsignedToken)); if (!isValid) return false; const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); const now = Math.floor(Date.now() / 1000); return payload.exp > now; } catch { return false; }}
// 动态生成管理界面 HTML,将管理路径注入前端脚本function getAdminHtml(adminPath) { return `<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>内容管理 · 控制台</title> <style> :root { --bg: #0a0a14; --surface: #111122; --card-bg: rgba(18, 18, 40, 0.75); --text: #e8e8f0; --text-secondary: #9a9ab8; --accent: #8b7cf7; --accent-glow: rgba(139, 124, 247, 0.25); --border: rgba(255, 255, 255, 0.08); --border-focus: rgba(139, 124, 247, 0.5); --danger: #f56c6c; --radius-sm: 10px; --radius-md: 16px; --radius-lg: 20px; --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', system-ui, -apple-system, sans-serif; background: var(--bg); background-image: radial-gradient(ellipse at 30% 20%, rgba(139, 124, 247, 0.04) 0%, transparent 60%), radial-gradient(ellipse at 70% 70%, rgba(100, 140, 220, 0.03) 0%, transparent 55%), radial-gradient(ellipse at 50% 50%, rgba(180, 160, 255, 0.02) 0%, transparent 70%); color: var(--text); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; letter-spacing: 0.01em; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* 背景装饰光斑 */ body::before { content: ''; position: fixed; top: -120px; left: -120px; width: 350px; height: 350px; border-radius: 50%; background: rgba(139, 124, 247, 0.06); filter: blur(100px); pointer-events: none; z-index: 0; animation: floatOrb 18s ease-in-out infinite; } body::after { content: ''; position: fixed; bottom: -100px; right: -100px; width: 300px; height: 300px; border-radius: 50%; background: rgba(120, 140, 230, 0.05); filter: blur(90px); pointer-events: none; z-index: 0; animation: floatOrb 22s ease-in-out infinite reverse; } @keyframes floatOrb { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(40px, -30px); } 50% { transform: translate(-20px, 25px); } 75% { transform: translate(-35px, -20px); } }
/* ---------- 卡片容器 ---------- */ .admin-card { position: relative; z-index: 1; background: var(--card-bg); backdrop-filter: blur(24px) saturate(130%); -webkit-backdrop-filter: blur(24px) saturate(130%); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 36px 32px 32px; max-width: 620px; width: 100%; box-shadow: 0 2px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.03) inset, 0 0 80px -20px rgba(139, 124, 247, 0.12); transition: box-shadow var(--transition), border-color var(--transition); } .admin-card:hover { border-color: rgba(255, 255, 255, 0.13); box-shadow: 0 4px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04) inset, 0 0 100px -18px rgba(139, 124, 247, 0.18); }
/* ---------- 头部区域 ---------- */ .card-header { display: flex; align-items: center; gap: 14px; margin-bottom: 28px; padding-bottom: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .card-header .icon-circle { width: 44px; height: 44px; border-radius: 50%; background: rgba(139, 124, 247, 0.12); display: flex; align-items: center; justify-content: center; font-size: 1.35rem; flex-shrink: 0; border: 1px solid rgba(139, 124, 247, 0.2); } .card-header h2 { font-size: 1.25rem; font-weight: 650; letter-spacing: -0.01em; color: #f0f0f8; margin: 0; line-height: 1.2; } .card-header .subtitle { font-size: 0.78rem; color: var(--text-secondary); font-weight: 400; letter-spacing: 0.02em; }
/* ---------- 表单 ---------- */ .input-group { margin-bottom: 20px; } .input-group label { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 0.82rem; font-weight: 550; color: #c5c5da; letter-spacing: 0.02em; text-transform: uppercase; } .input-group label .label-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); display: inline-block; flex-shrink: 0; box-shadow: 0 0 8px var(--accent-glow); } .input-group input, .input-group textarea { width: 100%; padding: 12px 16px; font-size: 0.95rem; font-family: inherit; background: rgba(255, 255, 255, 0.03); border: 1.5px solid rgba(255, 255, 255, 0.12); border-radius: var(--radius-sm); color: var(--text); outline: none; transition: all var(--transition); letter-spacing: 0.015em; line-height: 1.5; } .input-group input::placeholder, .input-group textarea::placeholder { color: rgba(200, 200, 220, 0.3); letter-spacing: 0.03em; } .input-group input:focus, .input-group textarea:focus { border-color: var(--border-focus); background: rgba(139, 124, 247, 0.04); box-shadow: 0 0 0 5px rgba(139, 124, 247, 0.06), 0 0 20px -6px var(--accent-glow); } .input-group textarea { min-height: 320px; font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', 'Courier New', monospace; font-size: 0.8rem; resize: vertical; line-height: 1.65; tab-size: 2; }
/* ---------- 按钮 ---------- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: linear-gradient(135deg, #8b7cf7 0%, #6d5fdd 50%, #5a4ecf 100%); color: #fff; border: none; padding: 13px 28px; border-radius: 28px; cursor: pointer; font-weight: 600; font-size: 0.95rem; letter-spacing: 0.02em; transition: all var(--transition); box-shadow: 0 4px 18px rgba(139, 124, 247, 0.3), 0 0 0 0 rgba(139, 124, 247, 0.4); position: relative; overflow: hidden; font-family: inherit; width: 100%; margin-top: 4px; } .btn::after { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 40%, transparent 60%, rgba(255, 255, 255, 0.06) 100%); border-radius: 28px; pointer-events: none; transition: opacity var(--transition); } .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(139, 124, 247, 0.4), 0 0 0 4px rgba(139, 124, 247, 0.08); background: linear-gradient(135deg, #9889f8 0%, #7a6de8 50%, #6b5fda 100%); } .btn:active:not(:disabled) { transform: translateY(0) scale(0.98); box-shadow: 0 2px 10px rgba(139, 124, 247, 0.3); transition: all 0.1s ease; } .btn:disabled { opacity: 0.45; cursor: not-allowed; filter: grayscale(15%); box-shadow: 0 2px 8px rgba(139, 124, 247, 0.15); transform: none; } .btn .btn-icon { font-size: 1.1rem; flex-shrink: 0; }
/* ---------- 状态提示 ---------- */ .status-msg { margin-top: 14px; font-size: 0.82rem; padding: 10px 14px; border-radius: 8px; letter-spacing: 0.02em; transition: all var(--transition); text-align: center; font-weight: 500; min-height: 20px; line-height: 1.4; } .status-msg:empty { display: none; } .status-msg.success { background: rgba(76, 200, 140, 0.1); color: #7ee8b8; border: 1px solid rgba(76, 200, 140, 0.2); } .status-msg.error { background: rgba(245, 108, 108, 0.1); color: #f9a0a0; border: 1px solid rgba(245, 108, 108, 0.2); } #loginStatus, #saveStatus { transition: all var(--transition); }
/* ---------- 可见性切换 ---------- */ .hidden { display: none !important; }
/* ---------- 次要操作行 ---------- */ .action-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 6px; } .action-hint { font-size: 0.72rem; color: var(--text-secondary); letter-spacing: 0.03em; opacity: 0.7; }
/* ---------- 响应式 ---------- */ @media (max-width: 500px) { .admin-card { padding: 24px 18px 20px; border-radius: var(--radius-md); } .card-header { gap: 10px; margin-bottom: 20px; padding-bottom: 16px; } .card-header .icon-circle { width: 36px; height: 36px; font-size: 1.1rem; } .card-header h2 { font-size: 1.1rem; } .input-group textarea { min-height: 240px; font-size: 0.75rem; } .btn { padding: 12px 20px; font-size: 0.9rem; border-radius: 24px; } } </style></head><body>
<!-- ==================== 登录卡片 ==================== --> <div class="admin-card" id="loginBox"> <div class="card-header"> <div class="icon-circle">🔐</div> <div> <h2>管理登录</h2> <div class="subtitle">请输入密码以继续</div> </div> </div> <div class="input-group"> <label><span class="label-dot"></span>管理密码</label> <input type="password" id="adminPassword" placeholder="输入管理密码..." autocomplete="current-password"> </div> <button class="btn" id="loginBtn"> <span class="btn-icon">→</span> 登 录 </button> <div id="loginStatus" class="status-msg"></div> </div>
<!-- ==================== 编辑卡片 ==================== --> <div class="admin-card hidden" id="editorBox"> <div class="card-header"> <div class="icon-circle">✏️</div> <div> <h2>编辑受保护内容</h2> <div class="subtitle">修改下方 HTML 并保存</div> </div> </div> <div class="input-group"> <label><span class="label-dot"></span>HTML 内容</label> <textarea id="htmlContent" placeholder="在此粘贴或编辑完整的 HTML 内容..." spellcheck="false"></textarea> </div> <div class="action-row"> <span class="action-hint">💡 保存后约 1-2 秒生效</span> </div> <button class="btn" id="saveBtn"> <span class="btn-icon">💾</span> 保存更改 </button> <div id="saveStatus" class="status-msg"></div> </div>
<!-- ==================== 脚本(功能逻辑不变) ==================== --> <script> const loginBox = document.getElementById('loginBox'); const editorBox = document.getElementById('editorBox'); const adminPassword = document.getElementById('adminPassword'); const loginBtn = document.getElementById('loginBtn'); const loginStatus = document.getElementById('loginStatus'); const htmlContent = document.getElementById('htmlContent'); const saveBtn = document.getElementById('saveBtn'); const saveStatus = document.getElementById('saveStatus'); let adminToken = sessionStorage.getItem('admin_token');
async function checkLogin() { if (!adminToken) return false; try { const resp = await fetch('/admin/verify', { headers: { 'Authorization': 'Bearer ' + adminToken } }); return resp.ok; } catch { return false; } }
async function loadContent() { const resp = await fetch('/admin/content', { headers: { 'Authorization': 'Bearer ' + adminToken } }); if (resp.ok) { const data = await resp.json(); htmlContent.value = data.content; } else { saveStatus.textContent = '⚠️ 无法加载当前内容'; saveStatus.className = 'status-msg error'; } }
// 辅助:设置状态样式 function setStatus(el, msg, type) { el.textContent = msg; el.className = 'status-msg ' + (type || ''); }
loginBtn.addEventListener('click', async () => { const pw = adminPassword.value.trim(); if (!pw) { setStatus(loginStatus, '请输入密码', 'error'); return; } loginBtn.disabled = true; setStatus(loginStatus, '验证中...', ''); try { const resp = await fetch('/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const data = await resp.json(); if (data.success) { adminToken = data.token; sessionStorage.setItem('admin_token', adminToken); setStatus(loginStatus, '', ''); loginBox.classList.add('hidden'); editorBox.classList.remove('hidden'); await loadContent(); } else { setStatus(loginStatus, '❌ 密码错误', 'error'); } } catch { setStatus(loginStatus, '网络错误,请重试', 'error'); } loginBtn.disabled = false; });
saveBtn.addEventListener('click', async () => { const content = htmlContent.value; saveBtn.disabled = true; setStatus(saveStatus, '保存中...', ''); try { const resp = await fetch('/admin/content', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + adminToken }, body: JSON.stringify({ content }) }); if (resp.ok) { setStatus(saveStatus, '✅ 已保存 (生效延迟约 1-2 秒)', 'success'); } else { setStatus(saveStatus, '❌ 保存失败', 'error'); } } catch { setStatus(saveStatus, '网络错误,请重试', 'error'); } saveBtn.disabled = false; });
// 自动恢复登录态 (async () => { if (adminToken && await checkLogin()) { loginBox.classList.add('hidden'); editorBox.classList.remove('hidden'); await loadContent(); } })();
// 回车键快速登录 adminPassword.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); loginBtn.click(); } }); </script></body></html>`;}
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' };
if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); }
// 获取管理路径(从环境变量读取,若未设置则使用默认值 /admin) const ADMIN_PATH = env.ADMIN_PATH || '/admin';
// ========== 管理相关路由 ========== // 管理界面首页(支持带或不带尾部斜杠) if (url.pathname === ADMIN_PATH || url.pathname === ADMIN_PATH + '/') { return new Response(getAdminHtml(ADMIN_PATH), { headers: { 'Content-Type': 'text/html; charset=utf-8', corsHeaders } }); }
// 管理员登录 if (request.method === 'POST' && url.pathname === ADMIN_PATH + '/login') { const { password } = await request.json(); const ADMIN_PW = env.ADMIN_PASSWORD; if (!ADMIN_PW) return new Response(JSON.stringify({ error: 'Admin password not set' }), { status: 500, headers: corsHeaders }); if (password === ADMIN_PW) { const token = await generateToken(env.SECRET_KEY + 'admin'); return new Response(JSON.stringify({ success: true, token }), { headers: corsHeaders }); } return new Response(JSON.stringify({ success: false }), { status: 401, headers: corsHeaders }); }
// 验证管理员 token if (request.method === 'GET' && url.pathname === ADMIN_PATH + '/verify') { const auth = request.headers.get('Authorization')?.split(' ')[1]; if (!auth || !(await verifyToken(auth, env.SECRET_KEY + 'admin'))) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 403, headers: corsHeaders }); } return new Response(JSON.stringify({ success: true }), { headers: corsHeaders }); }
// 获取当前保存的内容 if (request.method === 'GET' && url.pathname === ADMIN_PATH + '/content') { const auth = request.headers.get('Authorization')?.split(' ')[1]; if (!auth || !(await verifyToken(auth, env.SECRET_KEY + 'admin'))) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 403, headers: corsHeaders }); } const content = await env.CONTENT_KV.get('protected_html') || ''; return new Response(JSON.stringify({ content }), { headers: corsHeaders }); }
// 更新内容 if (request.method === 'PUT' && url.pathname === ADMIN_PATH + '/content') { const auth = request.headers.get('Authorization')?.split(' ')[1]; if (!auth || !(await verifyToken(auth, env.SECRET_KEY + 'admin'))) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 403, headers: corsHeaders }); } const { content } = await request.json(); if (typeof content !== 'string') return new Response(JSON.stringify({ error: 'invalid content' }), { status: 400, headers: corsHeaders }); await env.CONTENT_KV.put('protected_html', content); return new Response(JSON.stringify({ success: true }), { headers: corsHeaders }); }
// ========== 用户验证密码 ========== if (request.method === 'POST' && url.pathname === '/check') { const { password } = await request.json(); const CORRECT_PW = env.SECRET_PASSWORD; if (!CORRECT_PW) return new Response(JSON.stringify({ error: 'server config error' }), { status: 500, headers: corsHeaders }); if (password === CORRECT_PW) { const token = await generateToken(env.SECRET_KEY); return new Response(JSON.stringify({ success: true, token }), { headers: corsHeaders }); } return new Response(JSON.stringify({ error: 'wrong password' }), { status: 401, headers: corsHeaders }); }
// ========== 用户获取受保护内容(Token 校验) ========== if (request.method === 'GET' && url.pathname === '/content') { const auth = request.headers.get('Authorization')?.split(' ')[1]; if (!auth || !(await verifyToken(auth, env.SECRET_KEY))) { return new Response(JSON.stringify({ error: 'invalid or expired token' }), { status: 403, headers: corsHeaders }); } const content = await env.CONTENT_KV.get('protected_html'); if (!content) { return new Response('<p>暂无内容,请管理员上传。</p>', { headers: { 'Content-Type': 'text/html; charset=utf-8', corsHeaders } }); } return new Response(content, { headers: { 'Content-Type': 'text/html; charset=utf-8', corsHeaders } }); }
return new Response('Not Found', { status: 404, headers: corsHeaders }); }};
注意:代码中用到了
CONTENT_KV(KV 绑定变量名)和ADMIN_PASSWORD(环境变量),这些必须和你前面设置的名字完全一致。
如下,它会自动通过 Worker 获取 KV 里的最新内容。
xxxxxxxxxx<html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>安全访问</title> <style> :root { --bg: #0a0a14; --card-bg: rgba(22,22,40,0.7); --text: #e8e8f0; --text-secondary: #9898b4; --accent: #7c6ff7; --error: #f54b5e; --success: #3dd6a8; --radius: 18px; } * { margin:0; padding:0; box-sizing:border-box; } body { font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: var(--bg); min-height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; } .bg-orb { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.3; pointer-events: none; z-index:0; } .bg-orb--1 { width: 300px; height: 300px; background: radial-gradient(#7c6ff7, transparent 70%); top: -10%; left: -10%; animation: float1 20s infinite; } .bg-orb--2 { width: 250px; height: 250px; background: radial-gradient(#a78bfa, transparent 70%); bottom: -10%; right: -5%; animation: float2 18s infinite; } @keyframes float1 { 0%,100%{ transform: translate(0,0); } 50%{ transform: translate(40px,-20px); } } @keyframes float2 { 0%,100%{ transform: translate(0,0); } 50%{ transform: translate(-30px,30px); } }
.card { position: relative; z-index:1; background: var(--card-bg); backdrop-filter: blur(40px); border: 1px solid rgba(255,255,255,0.08); border-radius: var(--radius); padding: 36px 28px; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.5); max-width: 400px; width: 100%; transition: opacity 0.4s, transform 0.4s; } .card--hidden { opacity: 0; transform: scale(0.95); pointer-events: none; } h2 { color: var(--text); font-weight: 600; margin-bottom: 4px; } .subtitle { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 24px; } .input-group { position: relative; margin-bottom: 16px; } .input-group input { width: 100%; padding: 14px 48px 14px 16px; font-size: 1rem; color: var(--text); background: rgba(255,255,255,0.04); border: 1.5px solid rgba(255,255,255,0.12); border-radius: 12px; outline: none; transition: 0.2s; } .input-group input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(124,111,247,0.2); } .toggle-pw { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: none; border: none; color: rgba(255,255,255,0.4); cursor: pointer; padding: 6px; } .btn { width: 100%; padding: 14px; font-weight: 600; font-size: 1rem; background: linear-gradient(135deg, #7c6ff7, #5b4fcf); border: none; border-radius: 12px; color: white; cursor: pointer; transition: all 0.2s; box-shadow: 0 6px 20px rgba(124,111,247,0.3); } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn--loading { color: transparent; position: relative; } .btn--loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; margin: -10px 0 0 -10px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 0.7s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .error-msg { min-height: 20px; font-size: 0.8rem; color: var(--error); margin-top: 8px; opacity: 0; transition: opacity 0.2s; } .error-msg--visible { opacity: 1; } </style></head><body> <div class="bg-orb bg-orb--1"></div> <div class="bg-orb bg-orb--2"></div>
<div class="card" id="passwordCard"> <h2>🔐 受保护页面</h2> <p class="subtitle">请输入访问密码以获取内容</p> <div class="input-group"> <input type="password" id="passwordInput" placeholder="输入密码..." autocomplete="off"> <button class="toggle-pw" id="togglePwBtn" title="显示密码"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/> <circle cx="12" cy="12" r="3"/> </svg> </button> </div> <button class="btn" id="submitBtn" disabled>验证并获取内容</button> <div class="error-msg" id="errorMsg"></div> </div>
<script> (function() { // 🔴 请把这里替换成你自己的 Worker 地址! const WORKER_URL = 'https://你的worker域名'; // 例如 https://my-gate.xxx.workers.dev
const passwordCard = document.getElementById('passwordCard'); const passwordInput = document.getElementById('passwordInput'); const submitBtn = document.getElementById('submitBtn'); const errorMsg = document.getElementById('errorMsg'); const togglePwBtn = document.getElementById('togglePwBtn'); const SESSION_KEY = 'protected_content_token';
function showError(text) { errorMsg.textContent = text; errorMsg.classList.add('error-msg--visible'); } function clearError() { errorMsg.classList.remove('error-msg--visible'); }
function prepareBodyForContent() { // 移除密码页才需要的内联样式,保留新页面自身样式 document.body.removeAttribute('style'); // 删除背景光斑 document.querySelectorAll('.bg-orb').forEach(o => o.remove()); }
async function fetchContent(token) { try { const resp = await fetch(`${WORKER_URL}/content`, { headers: { 'Authorization': `Bearer ${token}` } }); if (resp.ok) { const html = await resp.text(); // 先清理 body,再替换内容 prepareBodyForContent(); document.body.innerHTML = html; // 关键修复:强制让 body 可以滚动 document.body.style.overflow = 'auto';
// 重新执行新页面中的内联脚本 const scripts = document.body.querySelectorAll('script'); scripts.forEach(oldScript => { const newScript = document.createElement('script'); Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)); newScript.appendChild(document.createTextNode(oldScript.innerHTML)); oldScript.parentNode.replaceChild(newScript, oldScript); }); return true; } else if (resp.status === 403) { showError('Token 已过期或无效,请重新输入密码'); sessionStorage.removeItem(SESSION_KEY); return false; } else { showError('获取内容失败'); return false; } } catch (e) { showError('网络错误,无法连接 Worker'); return false; } }
async function handleSubmit() { clearError(); const password = passwordInput.value.trim(); if (!password) return;
submitBtn.classList.add('btn--loading'); submitBtn.disabled = true; passwordInput.disabled = true;
try { const resp = await fetch(`${WORKER_URL}/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await resp.json(); if (resp.ok && data.success) { const token = data.token; sessionStorage.setItem(SESSION_KEY, token); const success = await fetchContent(token); if (!success) resetForm(); } else if (resp.status === 401) { showError('密码错误'); resetForm(); } else { showError(data.error || '验证失败'); resetForm(); } } catch (e) { showError('网络错误,请检查 Worker 地址'); resetForm(); } }
function resetForm() { submitBtn.classList.remove('btn--loading'); submitBtn.disabled = true; passwordInput.disabled = false; passwordInput.value = ''; passwordInput.focus(); }
passwordInput.addEventListener('input', () => { clearError(); submitBtn.disabled = !passwordInput.value.trim(); }); passwordInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleSubmit(); }); submitBtn.addEventListener('click', handleSubmit); togglePwBtn.addEventListener('click', () => { const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; passwordInput.setAttribute('type', type); });
// 会话恢复 (async function init() { const savedToken = sessionStorage.getItem(SESSION_KEY); if (savedToken) { const valid = await fetchContent(savedToken); if (!valid) sessionStorage.removeItem(SESSION_KEY); } if (document.getElementById('passwordCard')) { setTimeout(() => passwordInput.focus(), 400); } })(); })(); </script></body></html>
https://你的worker域名/admin(例如 https://password-gate.xxx.workers.dev/admin)。注意:这里的admin,是你在定义ADMIN_PATH时候的值,如:你的ADMIN_PATH是/my_admin_123,则在浏览器搜索:https://你的worker域名/my_admin_123ADMIN_PASSWORD)登录。你全程不需要登录 Cloudflare,也不需要接触代码,就像更新博客一样简单。
ADMIN_PASSWORD 和访问密码 SECRET_PASSWORD 不要相同。/admin 才能访问,且受管理密码保护,非常安全。By @Jrafina 2026-04-27 本博文内容为原创作品,未经允许不得转载。如需转载,请注明原作者及出处。