如何用Django生成30分钟有效、手机号唯一的基于时间的OTP
嘿,这个问题我太有共鸣了!之前做短信验证的时候也被这个坑过——用户狂点获取验证码,结果一堆短信过来,根本分不清哪个是有效的。不过基于时间窗口的OTP刚好能解决这个问题,给你分享个在Django里的实现方案:
核心思路
我们要实现的是同一个手机号在固定时间窗口(比如30分钟)内,无论请求多少次,生成的OTP都是完全相同的。核心逻辑是:
- 以「手机号 + 当前时间窗口标识 + 安全密钥」为输入,通过哈希函数生成固定的6位数字OTP
- 时间窗口标识由当前时间戳除以窗口秒数(30*60=1800秒)得到,同一个窗口内这个值不会变
实现代码
1. 生成基于时间的OTP
import hashlib import time from django.conf import settings def generate_time_based_otp(phone_number: str, window_minutes: int = 30) -> str: # 计算当前所属的时间窗口(每30分钟一个窗口) window_seconds = window_minutes * 60 current_window = int(time.time()) // window_seconds # 组合加密因子:手机号+时间窗口+安全密钥(密钥从settings读取,避免硬编码) data = f"{phone_number}{current_window}{settings.OTP_SECRET_KEY}" # 使用SHA-256哈希,确保结果唯一性和安全性 hash_obj = hashlib.sha256(data.encode('utf-8')) hash_hex = hash_obj.hexdigest() # 将十六进制哈希转成十进制,取最后6位作为OTP,不足6位补0 otp = str(int(hash_hex, 16))[-6:].zfill(6) return otp
2. 验证OTP的有效性
为了避免用户刚好在窗口切换时收到短信(比如30分钟整的时候),我们可以允许一定的时间漂移,比如验证当前窗口和前一个窗口的OTP:
def verify_time_based_otp(phone_number: str, otp: str, window_minutes: int = 30, allowed_drift_windows: int = 1) -> bool: window_seconds = window_minutes * 60 current_window = int(time.time()) // window_seconds # 遍历允许的时间窗口范围(默认包含当前窗口和前后各1个窗口) for window_offset in range(-allowed_drift_windows, allowed_drift_windows + 1): target_window = current_window + window_offset data = f"{phone_number}{target_window}{settings.OTP_SECRET_KEY}" hash_obj = hashlib.sha256(data.encode('utf-8')) hash_hex = hash_obj.hexdigest() expected_otp = str(int(hash_hex, 16))[-6:].zfill(6) if expected_otp == otp: return True return False
关键细节与注意事项
- 安全密钥配置:在
settings.py里添加一个随机的长字符串作为密钥,比如:
这个密钥一定要保密,不能泄露,否则别人可以伪造OTP。OTP_SECRET_KEY = get_random_string(50) # 可以用Django的get_random_string生成 - 无需存储OTP:因为OTP是实时计算的,不需要把OTP存在数据库里,既节省存储,又避免旧OTP的干扰。
- 请求频率限制:建议给获取验证码的接口加频率限制(比如同一个手机号1分钟内最多请求3次),防止恶意请求。
- 用户引导:在短信内容里加上提示,比如「此验证码30分钟内有效,无需重复获取」,减少用户重复请求的行为。
内容的提问来源于stack exchange,提问作者Harsh




