| 
 | 
 
分享一个自己写的纯网页版TOTP生成工具 
纯前端实现,密钥不离本地 
实时30秒倒计时可视化展示无需注册,即开即用 
以下是完整代码 
附运行截图 
         
 
html代码: 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
    <title>TOTP 倒计时</title> 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> 
    <style> 
        body { 
            font-family: 'Arial', sans-serif; 
            max-width: 500px; 
            margin: 0 auto; 
            padding: 20px; 
            text-align: center; 
            background: #f5f5f5; 
        } 
        input { 
            padding: 12px; 
            width: 300px; 
            margin: 15px 0; 
            font-size: 16px; 
            border: 2px solid #ddd; 
            border-radius: 4px; 
        } 
        button { 
            padding: 12px 25px; 
            background: #4285f4; 
            color: white; 
            border: none; 
            border-radius: 4px; 
            font-size: 16px; 
            cursor: pointer; 
            transition: background 0.3s; 
        } 
        button:hover { 
            background: #3367d6; 
        } 
        .totp-display { 
            font-family: Arial, sans-serif; 
            font-weight: bold; 
            font-size: 48px; 
            margin: 20px 0; 
            letter-spacing: 5px; 
            transition: color 0.3s; 
        } 
        .totp-display.green { 
            color: #4CAF50; 
        } 
        .totp-display.blue { 
            color: #2196F3; 
        } 
        .totp-display.red { 
            color: #f44336; 
            animation: pulse 0.5s infinite alternate; 
        } 
        .countdown-container { 
            position: relative; 
            width: 120px; 
            height: 120px; 
            margin: 30px auto; 
        } 
        .countdown-circle { 
            width: 100%; 
            height: 100%; 
        } 
        .countdown-circle-bg { 
            fill: none; 
            stroke: #e0e0e0; 
            stroke-width: 10; 
        } 
        .countdown-circle-fg { 
            fill: none; 
            stroke: #4CAF50; 
            stroke-width: 10; 
            stroke-linecap: round; 
            transform: rotate(-90deg); 
            transform-origin: 50% 50%; 
            transition: all 0.1s linear; 
        } 
        .countdown-circle-fg.blue { 
            stroke: #2196F3; 
        } 
        .countdown-circle-fg.red { 
            stroke: #f44336; 
        } 
        .countdown-text { 
            position: absolute; 
            top: 50%; 
            left: 50%; 
            transform: translate(-50%, -50%); 
            font-size: 30px; 
            font-weight: bold; 
            color: #333; 
        } 
        @keyframes pulse { 
            from { opacity: 1; } 
            to { opacity: 0.5; } 
        } 
    </style> 
</head> 
<body> 
    <h1>TOTP 验证码生成器</h1> 
    <p>请输入 Base32 密钥:</p> 
    <input type="text" id="secret" placeholder="例如:JBSWY3DPEHPK3PXP" /> 
    <button>生成动态验证码</button> 
       <div class="totp-display" id="result">000000</div> 
       <div class="countdown-container"> 
        <svg class="countdown-circle" viewBox="0 0 100 100"> 
            <circle class="countdown-circle-bg" cx="50" cy="50" r="45"/> 
            <circle class="countdown-circle-fg" id="countdown-circle" cx="50" cy="50" r="45"/> 
        </svg> 
        <div class="countdown-text" id="countdown">30</div> 
    </div> 
    <script> 
        // Base32 解码 
        function base32Decode(base32) { 
            const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 
            base32 = base32.replace(/[^A-Z2-7]/gi, '').toUpperCase(); 
            let bits = 0, value = 0, output = []; 
            for (let i = 0; i < base32.length; i++) { 
                const char = base32.charAt(i); 
                const index = alphabet.indexOf(char); 
                if (index === -1) continue; 
                value = (value << 5) | index; 
                bits += 5; 
                if (bits >= 8) { 
                    bits -= 8; 
                    output.push((value >>> bits) & 0xFF); 
                } 
            } 
            return output; 
        } 
        // 计算 HMAC-SHA1 
        function hmacSHA1Bytes(keyBytes, messageBytes) { 
            const key = CryptoJS.lib.WordArray.create(keyBytes); 
            const message = CryptoJS.lib.WordArray.create(messageBytes); 
            const hmac = CryptoJS.HmacSHA1(message, key); 
            return hmac.toString(CryptoJS.enc.Hex) 
                      .match(/.{1,2}/g) 
                      .map(byte => parseInt(byte, 16)); 
        } 
        // 动态截断 
        function dynamicTruncation(hmacBytes) { 
            const offset = hmacBytes[hmacBytes.length - 1] & 0x0F; 
            return ( 
                ((hmacBytes[offset]     & 0x7F) << 24) | 
                ((hmacBytes[offset + 1] & 0xFF) << 16) | 
                ((hmacBytes[offset + 2] & 0xFF) <<  8) | 
                 (hmacBytes[offset + 3] & 0xFF) 
            ); 
        } 
        // 计算 TOTP 
        function calculateTOTP(secret) { 
            try { 
                const keyBytes = base32Decode(secret); 
                if (keyBytes.length === 0) throw new Error("无效的 Base32 密钥"); 
                const timeStep = 30; 
                const timestamp = Math.floor(Date.now() / 1000); 
                const counter = Math.floor(timestamp / timeStep); 
                const counterBytes = new Array(8).fill(0); 
                for (let i = 0; i < 8; i++) { 
                    counterBytes[7 - i] = (counter >>> (i * 8)) & 0xFF; 
                } 
                const hmacBytes = hmacSHA1Bytes(keyBytes, counterBytes); 
                const binary = dynamicTruncation(hmacBytes); 
                return (binary % 1000000).toString().padStart(6, '0'); 
            } catch (e) { 
                return `错误: ${e.message}`; 
            } 
        } 
        // 更新倒计时和 TOTP 
        function updateTOTPAndCountdown() { 
            const secret = document.getElementById('secret').value.trim(); 
            if (!secret) return; 
            const timestamp = Math.floor(Date.now() / 1000); 
            const elapsed = timestamp % 30; 
            const remainingSeconds = 30 - elapsed; 
            const progress = elapsed / 30; 
           // 获取元素 
            const circle = document.getElementById('countdown-circle'); 
            const totpDisplay = document.getElementById('result'); 
            
            // 先移除所有颜色类 
            circle.classList.remove('blue', 'red'); 
            totpDisplay.classList.remove('green', 'blue', 'red'); 
              
            // 根据剩余时间设置不同颜色和效果 
            if (remainingSeconds > 20) { 
                // 30-21秒:绿色 
                circle.style.stroke = '#4CAF50'; 
                totpDisplay.classList.add('green'); 
            } else if (remainingSeconds > 5) { 
                // 20-6秒:蓝色 
                circle.style.stroke = '#2196F3'; 
                circle.classList.add('blue'); 
                totpDisplay.classList.add('blue'); 
            } else { 
                // 5-0秒:红色闪烁 
                circle.style.stroke = '#f44336'; 
                circle.classList.add('red'); 
                totpDisplay.classList.add('red'); 
            } 
              
            // 更新圆圈进度(逆时针减少) 
            const circumference = 2 * Math.PI * 45; 
            circle.style.strokeDasharray = circumference; 
            circle.style.strokeDashoffset = circumference * progress; 
              
            // 更新倒计时数字 
            document.getElementById('countdown').textContent = remainingSeconds; 
              
            // 更新 TOTP 
            document.getElementById('result').textContent = calculateTOTP(secret); 
 
            setTimeout(updateTOTPAndCountdown, 1000); 
        } 
 
        // 启动 TOTP 计算 
        function startTOTP() { 
            const secret = document.getElementById('secret').value.trim(); 
            if (!secret) { 
                alert("请输入 Base32 密钥!"); 
                return; 
            } 
              
            // 初始化圆圈和TOTP显示 
            const circle = document.getElementById('countdown-circle'); 
            const totpDisplay = document.getElementById('result'); 
            const circumference = 2 * Math.PI * 45; 
              
            circle.style.strokeDasharray = circumference; 
            circle.style.strokeDashoffset = 0; 
            circle.classList.remove('blue', 'red'); 
            circle.style.stroke = '#4CAF50'; 
              
            totpDisplay.classList.remove('blue', 'red'); 
            totpDisplay.classList.add('green'); 
              
            updateTOTPAndCountdown(); 
        } 
    </script> 
</body> 
</html> 
复制代码 |   
		
		
 	
  
 |