1. 准备工作
- cf账号
- 自定义域名托管到cf
- 准备二级域名
2fa.yourdomain.com,A解析到8.8.8.8
2. 创建workers
- cf面板
账号主页——计算和AI——Workers 和 Pages - 点击
创建应用程序 - 模板选择
从hello world!开始 - 配置
Worker name(本文以2fa为例),点部署
3. 绑定自定义域名
- 进入worker应用——设置
- 域和路由——添加——路由
- 区域:下拉列表挑一个用来解析的域名(为当前账号托管在cf平台的域名列表)
- 路由:
2fa.example.com/* - 点按钮
添加路由 - 绑定域名完成
4. 创建worker KV数据库
- CF面板账号主页——存储和数据库——workers KV
- Create Instance
- 命名空间名称:mykv
- 点创建,KV创建完成
4.1. 插入key/value(可选)
准备一张正SVG或者image:base64格式的LOGO图标,保存到KV数据库中
- key:2fa_logo
- value:<SVG的Path数据>或者<图片的base64数据>
5. 绑定kv数据配置namespace
- 进入worker应用——绑定
- 添加绑定——KV 命名空间——点击添加绑定按钮
- 变量名称:assert
- KV 命名空间:mykv
6. 导入代码部署
进入创建好的workder应用 2fa,编辑代码,填入以下代码:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
// TOTP 核心计算函数
class TOTP {
static async generate(secret, digits = 6, period = 30, timestamp = Date.now()) {
const key = this.base32Decode(secret.toUpperCase().replace(/\s+/g, ''))
if (!key) return 'Invalid Secret'
const time = Math.floor(timestamp / 1000 / period)
const buffer = new ArrayBuffer(8)
const view = new DataView(buffer)
view.setUint32(4, time, false)
try {
const hmac = await this.hmacSHA1(key, new Uint8Array(buffer))
const offset = hmac[hmac.length - 1] & 0x0F
const truncated = (
((hmac[offset] & 0x7F) << 24) |
((hmac[offset + 1] & 0xFF) << 16) |
((hmac[offset + 2] & 0xFF) << 8) |
(hmac[offset + 3] & 0xFF)
)
const code = truncated % (10 ** digits)
return code.toString().padStart(digits, '0')
} catch (error) {
return 'Error generating code'
}
}
static base32Decode(secret) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
let bits = ''
let result = []
for (let i = 0; i < secret.length; i++) {
const index = chars.indexOf(secret[i])
if (index === -1) return null
bits += index.toString(2).padStart(5, '0')
}
for (let i = 0; i + 8 <= bits.length; i += 8) {
const byte = parseInt(bits.substr(i, 8), 2)
result.push(byte)
}
return new Uint8Array(result)
}
static async hmacSHA1(key, data) {
const cryptoKey = await crypto.subtle.importKey(
'raw', key, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']
)
const signature = await crypto.subtle.sign('HMAC', cryptoKey, data)
return new Uint8Array(signature)
}
}
async function getLogoSrc() {
let logoSrc = ""; // 默认原始 Logo
try {
const logoData = await assert.get('2fa_logo') || "";
if (logoData) {
if (logoData.startsWith('<svg')) {
// 如果是 SVG 源码,转为 Data URL
const svgBase64 = btoa(unescape(encodeURIComponent(logoData)));
logoSrc = `data:image/svg+xml;base64,${svgBase64}`;
} else if (logoData.startsWith('data:') || logoData.length > 100) {
// 如果是 Base64 或已带前缀
logoSrc = logoData.startsWith('data:') ? logoData : `data:image/png;base64,${logoData}`;
}
}
} catch (e) {
console.error("KV Read Error:", e);
}
return logoSrc;
}
async function handleRequest(request) {
let logoSrc = await getLogoSrc(); // 默认原始 Logo
if (request.method === 'POST') {
const formData = await request.formData()
const secret = formData.get('secret') || ''
const digits = parseInt(formData.get('digits') || '6')
const period = parseInt(formData.get('period') || '30')
try {
const token = await TOTP.generate(secret, digits, period)
const timeLeft = period - (Math.floor(Date.now() / 1000) % period)
return new Response(JSON.stringify({
token,
timeLeft,
success: true
}), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({
error: 'Invalid secret key. Please use valid Base32 format.',
success: false
}), {
headers: { 'Content-Type': 'application/json' }
})
}
}
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2FA验证码生成器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#1E40AF',
neutral: '#1F2937',
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.token-animation { animation: pulse 30s linear infinite; }
.progress-bar { transition: width 1s linear; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.8; } 100% { opacity: 1; } }
}
</style>
</head>
<body class="bg-gray-50 font-inter min-h-screen">
<header class="bg-gradient-to-r from-primary to-secondary text-white shadow-lg">
<div class="container mx-auto px-4 py-6">
<div class="flex justify-between items-center">
<div class="flex items-center space-x-3">
<img src="${logoSrc}" alt="Logo" class="h-10 w-10 rounded-xl shadow-md border border-white/20">
<h1 class="text-2xl md:text-3xl font-bold">2FA 验证码生成器</h1>
</div>
<div class="bg-white/20 px-3 py-1 rounded-full text-sm">
<i class="fa fa-lock mr-1"></i> 本地计算,隐私保护
</div>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-6">
<div class="max-w-2xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="bg-neutral text-white p-6 text-center relative">
<div class="absolute top-4 right-4 text-sm opacity-70" id="timeLeft">
<i class="fa fa-clock-o mr-1"></i> <span>30</span>s
</div>
<div class="w-full h-1 bg-gray-600 absolute top-0 left-0 overflow-hidden">
<div id="progressBar" class="progress-bar bg-primary h-full w-full"></div>
</div>
<h2 class="text-sm uppercase tracking-wider opacity-70 mb-3">当前验证码</h2>
<div id="tokenDisplay" class="text-5xl md:text-6xl font-bold tracking-widest mb-4 token-animation">
000000
</div>
<p class="text-gray-300 text-sm">
<i class="fa fa-info-circle mr-1"></i> 验证码每30秒更新一次
</p>
</div>
<div class="p-6">
<form id="totpForm" class="space-y-6">
<div>
<label for="secret" class="block text-sm font-medium text-gray-700 mb-1">
<i class="fa fa-key text-primary mr-1"></i> 密钥 (Base32)
</label>
<input
type="text"
id="secret"
name="secret"
placeholder="如:JBSWY3DPEHPK3PXP"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition"
required
>
<p class="text-xs text-gray-500 mt-1">
输入您的2FA密钥(不带空格),通常在扫码设置时提供
</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="digits" class="block text-sm font-medium text-gray-700 mb-1">
<i class="fa fa-hashtag text-primary mr-1"></i> 位数
</label>
<select
id="digits"
name="digits"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition"
>
<option value="6">6位数字</option>
<option value="8">8位数字</option>
</select>
</div>
<div>
<label for="period" class="block text-sm font-medium text-gray-700 mb-1">
<i class="fa fa-refresh text-primary mr-1"></i> 周期(秒)
</label>
<select
id="period"
name="period"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition"
>
<option value="30">30秒</option>
<option value="60">60秒</option>
</select>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-secondary text-white font-medium py-3 px-4 rounded-lg transition flex items-center justify-center"
>
<i class="fa fa-calculator mr-2"></i> 生成验证码
</button>
</form>
<div class="pt-6 border-t border-gray-100">
<h3 class="text-lg font-semibold text-gray-800 mb-3">
<i class="fa fa-lightbulb-o text-yellow-500 mr-2"></i> 使用说明
</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li class="flex items-start">
<i class="fa fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>输入从服务提供商获取的2FA密钥(非QR码图片)</span>
</li>
<li class="flex items-start">
<i class="fa fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>密钥通常是32位Base32格式字符串(不含空格)</span>
</li>
<li class="flex items-start">
<i class="fa fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>所有计算在本地完成,您的密钥不会发送到服务器</span>
</li>
<li class="flex items-start">
<i class="fa fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>遵循 RFC 6238 TOTP 标准</span>
</li>
</ul>
</div>
</div>
</div>
<div class="mt-2 bg-red-50 border-l-4 border-red-400 p-4 rounded-r-lg max-w-2xl mx-auto">
<div class="flex">
<div class="flex-shrink-0">
<i class="fa fa-info-circle text-red-500 text-xl"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">安全提示</h3>
<div class="mt-2 text-sm text-red-700">
<p>请勿在公共设备上使用此工具。建议将2FA密钥保存在安全的密码管理器中。</p>
</div>
</div>
</div>
</div>
</main>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('totpForm');
const tokenDisplay = document.getElementById('tokenDisplay');
const timeLeftDisplay = document.querySelector('#timeLeft span');
const progressBar = document.getElementById('progressBar');
let refreshInterval = null;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (refreshInterval) clearInterval(refreshInterval);
const formData = new FormData(form);
try {
const response = await fetch('/', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
tokenDisplay.textContent = result.token;
timeLeftDisplay.textContent = result.timeLeft;
updateProgressBar(result.timeLeft, formData.get('period'));
refreshInterval = setInterval(() => {
let currentTime = parseInt(timeLeftDisplay.textContent);
if (currentTime <= 1) {
form.dispatchEvent(new Event('submit'));
} else {
timeLeftDisplay.textContent = currentTime - 1;
updateProgressBar(currentTime - 1, formData.get('period'));
}
}, 1000);
} else {
alert(result.error);
}
} catch (error) {
alert('生成验证码失败: ' + error.message);
}
});
function updateProgressBar(timeLeft, period = 30) {
progressBar.style.width = (timeLeft / period) * 100 + '%';
}
window.addEventListener('beforeunload', () => {
if (refreshInterval) clearInterval(refreshInterval);
});
});
</script>
</body>
</html>`
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
}