Flask集成pyotp生成动态口令

Python中的pyotp模块是一个用于生成和验证一次性密码(OTP)的库,支持基于时间(TOTP)和计数器(HOTP)的两种主流算法。它遵循RFC 4226(HOTP)和RFC 6238(TOTP)标准,兼容Google Authenticator等主流认证工具,广泛应用于需要增强系统安全性的业务场景中。

后端 python 代码

python 复制代码
# main.py
import pyotp
import base64
import qrcode
import logging
from io import BytesIO
from flask import Flask, request, jsonify, render_template

app = Flask(__name__)

app.config['JSON_AS_ASCII'] = False

# 禁止控制台输出请求信息
logging.getLogger('werkzeug').disabled = True
# 设置总的日志输出级别
app.logger.setLevel(logging.ERROR)

# 404错误处理
@app.errorhandler(404)
def page_not_found(e):
    # 直接返回字符串
    return "您请求的资源不存在!", 404

# 500错误处理
@app.errorhandler(500)
def internal_error(e):
    return "服务器内部错误!", 500

# 捕获所有未处理异常
@app.errorhandler(Exception)
def handle_exception(e):
    return "发生错误,请稍后再试!", 500

@app.route('/')
def process_request():
    return render_template('index.html')

@app.route('/generate_secret', methods=['POST'])
def generate_secret():
    secret = pyotp.random_base32()
    response = {'status': 0, 'data': secret}
    return jsonify(response)

@app.route('/generate_otp', methods=['POST'])
def generate_otp():
    response = {'status': 1, 'data': None, 'msg': None}
    try:
        data = request.json
        secret = data['secret']
        length = int(data['length'])
        interval = int(data['interval'])
        
        if not verify_base32_key(secret):
            response['msg'] = '无效的密钥'
            return jsonify(response)
        
        if length>10 or length<4:
            response['msg'] = '口令的有效长度为 4 ~ 10 位'
            return jsonify(response)
        
        if interval>60 or interval<30:
            response['msg'] = '口令有效期范围为 30 ~ 60 秒'
            return jsonify(response)

        otp_code = get_otp_code(secret, length, interval)

        # 生成二维码
        img = qrcode.make(otp_code)
        buffered = BytesIO()
        img.save(buffered, format="PNG")
        img_str = base64.b64encode(buffered.getvalue()).decode()
        
        response['status'] = 0
        response['data'] = {
            'otp_code': otp_code,
            'qrcode': f"data:image/png;base64,{img_str}"
        }
        return jsonify(response)
    except Exception as e:
        response['msg'] = f'动态口令生成异常: {str(e)}'
        return jsonify(response)
    
@app.route('/verify_otp', methods=['POST'])
def verify_otp():
    data = request.json
    secret = data['secret']
    otp_code = data['otp_code']
    interval = int(data['interval'])
    response = {'status': 0, 'data': verify_otp_code(secret, otp_code, interval)}
    return jsonify(response)


'''
生成动态口令
secret_key 32位密钥字符串
digits 动态口令长度
interval 口令有效期
'''
def get_otp_code(secret_key, digits=6, interval=30):
    totp = pyotp.TOTP(secret_key, digits=digits, interval=interval)
    otp_code = totp.now()
    return otp_code

'''
校验动态口令
secret_key 32位密钥字符串
totp_code 动态口令
interval 口令有效期
'''
def verify_otp_code(secret_key, totp_code, interval=30):
    totp = pyotp.TOTP(secret_key, digits=len(totp_code), interval=interval)
    is_valid = totp.verify(totp_code)
    return is_valid

# 验证32位密钥, 标准的Base32字母表为A-Z和2-7,不含小写字母或特殊符号
def verify_base32_key(key):
    allowed = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
    if len(key) != 32 or not all(c in allowed for c in key):
        return False
    try:
        base64.b32decode(key, casefold=False)
        return True
    except:
        return False

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8181)

前端 html 代码

在项目根目录下新建一个templates模板目录,然后在里面创建一个html文件,名称为 index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
    <title>动态口令生成器</title>
    <style>
        :root {
            --primary-color: #2196F3;
            --secondary-color: #64B5F6;
            --background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', system-ui;
        }

        body {
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            background: var(--background);
            padding: 20px;
        }

        .container {
            background: rgba(255, 255, 255, 0.95);
            padding: 2rem;
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.1);
            width: 100%;
            max-width: 600px;
            transition: transform 0.3s ease;
        }

        .form-group {
            margin-bottom: 1.5rem;
        }

        label {
            display: block;
            margin-bottom: 0.5rem;
            color: #2c3e50;
            font-weight: 500;
        }

        input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 16px;
            transition: border-color 0.3s ease;
        }

        input:focus {
            outline: none;
            border-color: var(--primary-color);
            box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
        }

        button {
            background: var(--primary-color);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s ease;
            width: 100%;
        }

        button:hover {
            background: var(--secondary-color);
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(33, 150, 243, 0.3);
        }

        #result {
            margin-top: 0rem;
            text-align: center;
        }

        #qrcode {
			display: none;
            margin: 0px auto;
			justify-content: center;
			align-items: center;
			width: 100%;
        }

        #timer {
            font-size: 16px;
            color: #e74c3c;
            margin-top: 1rem;
        }
		
		#error {
			color: #e74c3c;
		}
		
		#info {
			color: #009688;
			margin-top: 1rem;
		}
		
		.tab-nav {
			list-style: none;
			display: flex;
			margin-bottom: 2rem;
			border-bottom: 2px solid #eee;
		}

		.tab-nav li {
			padding: 12px 24px;
			cursor: pointer;
			color: #666;
			transition: all 0.3s ease;
			border-bottom: 2px solid transparent;
		}

		.tab-nav li.active {
			color: var(--primary-color);
			border-bottom-color: var(--primary-color);
		}

		.tab-content {
			display: none;
		}

		.tab-content.active {
			display: block;
		}
		
		.result-box {
			display: none;
			background: #f8f9fa;
			border-radius: 8px;
			padding: 1rem;
			text-align: center;
		}

		pre {
			white-space: pre-wrap;
			word-wrap: break-word;
			background: #fff;
			border-radius: 6px;
		}

		.copy-btn {
			margin-top: 2rem;
			background: #2F4056 !important;
			width: auto !important;
			display: inline-block !important;
		}

        @media (max-width: 480px) {
            .container {
                padding: 1.5rem;
            }
            
            input, button {
                font-size: 14px;
            }
        }
    </style>
</head>
<body>
	<div class="container">
		<ul class="tab-nav">
			<li class="active" data-tab="create-tab">生成密钥</li>
			<li data-tab="generate-tab">生成口令</li>
			<li data-tab="verify-tab">口令验证</li>
		</ul>
		
		<!-- 密钥生成界面 -->
		<div id="create-tab" class="tab-content active">
			<p style="color: #009688; line-height: 30px; font-size: 14px;">声明:本系统不会存储任何密钥,请妥善保管,切勿外泄!如若丢失,请重新生成,并同步更新客户端和服务端的密钥信息。</p>
			<button style="margin-top: 2rem; margin-bottom: 1.5rem;" onclick="generateSecret()">生成密钥</button>
			<div class="result-box">
				<pre id="new-secret"></pre>
				<button class="copy-btn" onclick="copySecrett()">复制密钥</button>
				<p id="info"></p>
			</div>
		</div>

		<!-- 动态口令生成界面 -->
		<div id="generate-tab" class="tab-content">
			<div class="form-group">
				<label for="secret">密钥:</label>
				<input type="text" id="secret" placeholder="请输入32位密钥" required>
			</div>
			
			<div class="form-group">
				<label for="length">动态口令长度:</label>
				<input type="number" id="length" min="4" max="10" value="6" placeholder="请输入动态口令长度,有效范围 4 ~ 10 位" required>
			</div>
			
			<div class="form-group">
				<label for="length">口令有效期(秒):</label>
				<input type="number" id="interval" min="30" max="60" value="60" placeholder="请输入动态口令有效期,时间范围 30 ~ 60 秒" required>
			</div>

			<button onclick="generateOTP()">生成动态口令</button>

			<div id="result">
				<div id="qrcode">
					<img id="qrImage" src="" alt="二维码">
				</div>
				<p id="otp_code"></p>
				<p id="timer"></p>
				<p id="error"></p>
			</div>
		</div>
		
		<!-- 口令验证界面 -->
		<div id="verify-tab" class="tab-content">
			<p style="color: #009688; line-height: 30px; font-size: 14px; margin-bottom: 1.5rem;">提示:验证时,必须同时设置密钥、口令和有效期参数,并确保与口令生成时指定的参数一致!</p>
			<div class="form-group">
				<label for="secret">密钥:</label>
				<input type="text" id="verify-secret" placeholder="请输入32位密钥" required>
			</div>
			<div class="form-group">
				<label for="secret">动态口令:</label>
				<input type="text" id="verify-otp_code" placeholder="请输入动态口令" required>
			</div>
			<div class="form-group">
				<label for="secret">口令有效期(秒):</label>
				<input type="number" id="verify-interval" min="30" max="60" value="60" placeholder="请输入动态口令有效期,时间范围 30 ~ 60 秒" required>
			</div>
			<button style="margin-top: 2rem; margin-bottom: 1.5rem;" onclick="verifyOTP()">验证</button>
			<div id="verify-info" style="text-align: center;"></div>
		</div>
	</div>

    <script>
        let countdown;
		
		document.querySelectorAll('.tab-nav li').forEach(tab => {
			tab.addEventListener('click', function() {
				// 移除所有激活状态
				document.querySelectorAll('.tab-nav li, .tab-content').forEach(el => {
					el.classList.remove('active');
				});
				
				// 设置当前激活状态
				this.classList.add('active');
				document.getElementById(this.dataset.tab).classList.add('active');
			});
		});
		
		// 生成密钥
		async function generateSecret() {
			document.getElementsByClassName('result-box')[0].style.display = 'none';
			document.getElementById('info').innerHTML = '';
			try {
                const response = await fetch('/generate_secret', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    }
                });

                const resp = await response.json();
				
				const data = resp.data;
				document.getElementsByClassName('result-box')[0].style.display = 'block';
				document.getElementById('new-secret').textContent = data;
            } catch (error) {
                console.error('Error:', error);
            }
		}
		
		// 复制密钥
		function copySecrett() {
			const secret = document.getElementById('new-secret').textContent;
			navigator.clipboard.writeText(secret);
			document.getElementById('info').innerHTML = '复制成功';
		}

		// 生成动态口令
        async function generateOTP() {
			document.getElementById('error').innerHTML = '';
            const secret = document.getElementById('secret').value;
            const length = document.getElementById('length').value;
			const interval = document.getElementById('interval').value;

            try {
                const response = await fetch('/generate_otp', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ secret, length, interval })
                });

                const resp = await response.json();
				
				const data = resp.data;
				
				if (resp.status == 1) {
					clearInterval(countdown);
					document.getElementById('timer').innerHTML = '';
					emptyInfo()
					document.getElementById('error').innerHTML = `${resp.msg}`;
					return;
				}
                
                document.getElementById('otp_code').innerHTML = `动态口令:<strong>${data.otp_code}</strong>`;
				document.getElementById('qrImage').src = `${data.qrcode}`;
				document.getElementById('qrcode').style.display = 'flex';

                // 启动倒计时
                startCountdown(interval);
            } catch (error) {
                console.error('Error:', error);
            }
        }
		
		
		// 验证动态口令
        async function verifyOTP() {
			document.getElementById('verify-info').innerHTML = '';
            const secret = document.getElementById('verify-secret').value;
            const otp_code = document.getElementById('verify-otp_code').value;
			const interval = document.getElementById('verify-interval').value;

            try {
                const response = await fetch('/verify_otp', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ secret, otp_code, interval })
                });

                const resp = await response.json();
				
				if (resp.data) {
					document.getElementById('verify-info').innerHTML = '<p style="color: #009688;">口令正确,验证成功</p>';
				} else {
					document.getElementById('verify-info').innerHTML = '<p style="color: #e74c3c;">口令错误或已失效,验证失败</p>';
				}
                
            } catch (error) {
                console.error('Error:', error);
            }
        }

        function startCountdown(seconds) {
            let remaining = seconds;
            const timerElement = document.getElementById('timer');
            
            clearInterval(countdown);
            
            countdown = setInterval(() => {
				remaining--;
				timerElement.textContent = `有效期:${remaining} 秒`;
				if (remaining <= 0) {
                    clearInterval(countdown);
                    timerElement.textContent = '口令已过期';
                }
            }, 1000);
        }
		
		function emptyInfo() {
			document.getElementById('qrcode').style.display = 'none';
			document.getElementById('otp_code').innerHTML = '';
			document.getElementById('qrImage').src = '';
		}
		
    </script>
</body>
</html>

界面截图

🏷️ 如有疑问,可以关注 我的知识库,直接提问即可。

相关推荐
_深海凉_6 分钟前
LeetCode热题100-颜色分类
python·算法·leetcode
AC赳赳老秦29 分钟前
OpenClaw email技能:批量发送邮件、自动回复,高效处理工作邮件
运维·人工智能·python·django·自动化·deepseek·openclaw
zhaoshuzhaoshu37 分钟前
Python 语法之数据结构详细解析
python
AI问答工程师1 小时前
Meta Muse Spark 的"思维压缩"到底是什么?我用 Python 复现了核心思路(附代码)
人工智能·python
zfan5202 小时前
python对Excel数据处理(1)
python·excel·pandas
小饕2 小时前
我从零搭建 RAG 学到的 10 件事
python
老歌老听老掉牙2 小时前
PyQt5+Qt Designer实战:可视化设计智能参数配置界面,告别手动布局时代!
python·qt
格鸰爱童话3 小时前
向AI学习项目技能(六)
java·人工智能·spring boot·python·学习
悟空爬虫-彪哥3 小时前
VRChat开发环境配置,零基础教程
python
数据知道3 小时前
《 Claude Code源码分析与实践》专栏目录
python·ai·github·claude code·claw code