一、准备工作
必要账号
Cloudflare账号(免费版即可)
域名(可选,用于自定义域名)
核心文件
保存本文末尾的完整代码为worker.js
二、部署到Cloudflare Workers
1. 创建Worker项目
登录Cloudflare控制台 → 选择「Workers 和 Pages」→ 点击「创建应用」→ 选择「创建Worker」
为项目命名(如
2fa-generator)→ 点击「部署」
2. 替换代码
部署完成后点击「编辑代码」
删除默认代码,粘贴
worker.js完整代码点击「保存并部署」→ 等待部署完成(约5秒)
点击「预览」可测试默认域名(格式:
https://[项目名].[用户名].workers.dev)
三、自定义域名配置(可选)
1. 添加域名到Cloudflare
进入Cloudflare控制台 → 「网站」→ 「添加站点」→ 输入你的域名
按提示修改域名DNS服务器(需在域名注册商处操作)
等待DNS生效(通常5-30分钟,部分域名可能需要24小时)
2. 绑定自定义域名到Worker
进入Worker项目 → 「触发器」→ 「添加自定义域」
输入子域名(如
2fa.yourdomain.com)→ 点击「添加」Cloudflare会自动配置SSL证书(免费),几分钟后即可通过自定义域名访问
四、功能测试与验证
访问部署地址,输入测试密钥
JBSWY3DPEHPK3PXP(Base32格式)点击「生成验证码」,应显示6位动态验证码(每30秒更新)
使用手机OTP应用(如Google Authenticator)扫描同一密钥,对比验证码是否一致
五、高级配置(可选)
1. 自定义Logo
替换代码中这一行的URL为你的logo地址:
<img src="https://proxy.mako.fun/cnabc.svg" alt="Logo" class="h-10 w-auto">
2. 修改主题颜色
调整Tailwind配置中的颜色值:
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6', // 主色调(蓝色)
secondary: '#1E40AF', // 辅助色(深蓝色)
neutral: '#1F2937', // 中性色(深灰)
}
}
}
}六、完整代码(Worker.js)
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 handleRequest(request) {
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="https://proxy.mako.fun/cnabc.svg" alt="Logo" class="h-10 w-auto">
<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-10">
<div class="max-w-2xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden">
<div class="bg-neutral text-white p-8 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="mt-8 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>
</ul>
</div>
</div>
</div>
<div class="mt-8 bg-blue-50 border-l-4 border-blue-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-blue-500 text-xl"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">安全提示</h3>
<div class="mt-2 text-sm text-blue-700">
<p>请勿在公共设备上使用此工具。建议将2FA密钥保存在安全的密码管理器中。</p>
</div>
</div>
</div>
</div>
</main>
<footer class="bg-neutral text-white py-6 mt-12">
<div class="container mx-auto px-4 text-center text-sm opacity-70">
<p>2FA 验证码生成器 | 基于 Cloudflare Workers 构建</p>
<p class="mt-1">遵循 RFC 6238 TOTP 标准</p>
</div>
</footer>
<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' },
})
}七、常见问题解决
验证码不刷新
→ 检查浏览器控制台是否有JavaScript错误
→ 确认系统时间与网络时间同步自定义域名无法访问
→ 检查DNS是否正确指向Cloudflare
→ 在「 Workers触发器」中确认域名状态为「活动」Logo无法显示
→ 使用绝对HTTPS链接
→ 检查图片URL是否支持跨域访问(可通过CDN加速)
通过以上步骤,你可以拥有一个完全自主可控的2FA验证码生成工具,所有计算