Cloudflare Workers 2FA验证码生成器部署教程
甲骨文 oracle 104

一、准备工作

  1. 必要账号

  2. 核心文件
    保存本文末尾的完整代码为 worker.js

二、部署到Cloudflare Workers

1. 创建Worker项目
  1. 登录Cloudflare控制台 → 选择「Workers 和 Pages」→ 点击「创建应用」→ 选择「创建Worker」

  2. 为项目命名(如 2fa-generator)→ 点击「部署」

2. 替换代码
  1. 部署完成后点击「编辑代码」

  2. 删除默认代码,粘贴 worker.js 完整代码

  3. 点击「保存并部署」→ 等待部署完成(约5秒)

  4. 点击「预览」可测试默认域名(格式:https://[项目名].[用户名].workers.dev

三、自定义域名配置(可选)

1. 添加域名到Cloudflare
  1. 进入Cloudflare控制台 → 「网站」→ 「添加站点」→ 输入你的域名

  2. 按提示修改域名DNS服务器(需在域名注册商处操作)

  3. 等待DNS生效(通常5-30分钟,部分域名可能需要24小时)

2. 绑定自定义域名到Worker
  1. 进入Worker项目 → 「触发器」→ 「添加自定义域」

  2. 输入子域名(如 2fa.yourdomain.com)→ 点击「添加」

  3. Cloudflare会自动配置SSL证书(免费),几分钟后即可通过自定义域名访问

四、功能测试与验证

  1. 访问部署地址,输入测试密钥 JBSWY3DPEHPK3PXP(Base32格式)

  2. 点击「生成验证码」,应显示6位动态验证码(每30秒更新)

  3. 使用手机OTP应用(如Google Authenticator)扫描同一密钥,对比验证码是否一致

五、高级配置(可选)

替换代码中这一行的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' },
  })
}

七、常见问题解决

  1. 验证码不刷新
    → 检查浏览器控制台是否有JavaScript错误
    → 确认系统时间与网络时间同步

  2. 自定义域名无法访问
    → 检查DNS是否正确指向Cloudflare
    → 在「 Workers触发器」中确认域名状态为「活动」

  3. Logo无法显示
    → 使用绝对HTTPS链接
    → 检查图片URL是否支持跨域访问(可通过CDN加速)

通过以上步骤,你可以拥有一个完全自主可控的2FA验证码生成工具,所有计算

Cloudflare Workers 2FA验证码生成器部署教程
https://www.cnabc.de/archives/HKVbBUpE
作者
Administrator
发布于
更新于
许可