MENU

利用cf的workers部署2FA动态码验证器

February 12, 2026 • 默认分类

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' },
    })
}