验证码、会话令牌、邀请链接和密码重置链接都需要不可预测的随机值。很多人会直接使用 random 生成字符串,但 random 面向模拟和一般程序逻辑,不适合安全场景。Python 标准库的 secrets 使用操作系统提供的安全随机源,适合生成令牌和比较敏感值。本文介绍令牌长度、编码方式、生命周期和使用边界。

一、不要用 random 生成安全令牌

random 的目标是可复现的伪随机序列。即使输出看起来随机,也不应把它用于登录令牌、找回密码链接或 API 密钥。安全场景应使用 secrets:

import secrets

token = secrets.token_urlsafe(32)
print(token)

token_urlsafe() 返回适合放在 URL 中的字符串,参数表示随机字节数,而不是最终字符数。编码后的字符串会比原始字节更长。令牌长度应该根据威胁模型、有效期和尝试限制选择,不要只看“字符串有多少位”。

二、选择不同的令牌函数

secrets 提供几种常用接口:

import secrets

print(secrets.token_bytes(16))
print(secrets.token_hex(16))
print(secrets.token_urlsafe(16))

token_bytes() 返回原始字节,适合后续二进制处理;token_hex() 返回十六进制字符串,便于日志和数据库字段存储;token_urlsafe() 更适合 URL 查询参数或路径。不要在日志中输出完整的生产令牌,即使令牌使用了安全随机源,也会因为泄露而失去安全性。

三、生成一次性密码和验证码

choice() 可以从指定字符集合中选择安全随机字符:

import secrets
import string


def generate_code(length=6):
    alphabet = string.digits
    return "".join(secrets.choice(alphabet) for _ in range(length))


print(generate_code())

短信验证码还需要有效期、失败次数限制、发送频率限制和使用后失效。随机生成并不能防止撞库、短信转发或接口刷取。验证码不要直接写入普通日志,数据库中也应考虑哈希存储或加密存储,并记录用途和过期时间。

如果验证码需要避免容易混淆的字符,可以从自定义字母表选择;但字母表越小,在相同长度下的搜索空间越小,需要提高长度或加强尝试限制。安全性来自随机空间和系统防护的组合。

四、密码重置链接的设计

密码重置令牌应当足够随机、一次性使用并设置较短有效期。数据库中可以保存令牌摘要,而不是保存完整令牌:

import hashlib
import secrets


raw_token = secrets.token_urlsafe(32)
token_digest = hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
print("发给用户的令牌:", raw_token)
print("数据库保存的摘要:", token_digest)

用户点击链接时,服务端对收到的令牌计算摘要,再查找未过期、未使用且匹配的记录。即使数据库只读泄露,攻击者也不能直接拿摘要当作完整重置令牌使用。生产设计还应防止令牌出现在 Referer、访问日志和浏览器历史中,并在成功重置后使旧会话失效。

五、安全比较和固定格式

比较令牌或签名时,不要只依赖普通字符串比较来处理敏感值。secrets.compare_digest() 可以比较相同类型的字符串或字节,并减少因比较时间差异造成的风险:

import secrets

expected = "a1b2c3"
received = "a1b2c3"
print(secrets.compare_digest(expected, received))

比较前要统一类型和格式,不要在一端比较十六进制字符串、另一端比较原始字节。固定长度、规范编码和明确的错误处理也很重要。安全比较并不能解决令牌太短、已泄露或没有过期时间的问题。

六、API 密钥和会话令牌的生命周期

令牌创建只是第一步,还需要管理状态:谁创建、授予什么权限、何时过期、是否可以撤销、最后一次使用时间是什么。高权限 API 密钥应支持轮换和撤销,不能永久有效。会话令牌应绑定必要的会话信息,并在密码修改、登出所有设备等操作后失效。

令牌应通过 HTTPS 传输,避免放在 URL 中长期暴露;浏览器场景要结合安全 Cookie、SameSite 和 CSRF 防护。服务端日志、监控事件和异常堆栈都需要进行脱敏。不要把“令牌是随机的”误认为“令牌泄露后没有影响”。

七、令牌长度与尝试限制

安全令牌的有效强度取决于随机空间和攻击者可尝试的次数。短令牌即使使用安全随机源,也可能被在线猜测。增加长度、缩短有效期、限制每个账户和 IP 的尝试次数、加入风控和告警,通常需要一起设计。对于六位验证码,在线尝试限制比单纯增加字符集更关键。

不要把令牌截短成便于阅读的几位字符后再认为它仍然拥有原始安全强度。如果业务必须展示短码,应使用独立的短期挑战机制,且有严格频率限制和绑定上下文。

八、secrets 不能替代密码哈希

secrets 适合生成随机盐、重置令牌和会话值,但不能直接用来保存用户密码。密码存储应使用专门的慢速密码哈希方案,例如 Argon2、scrypt、bcrypt 或平台提供的密码哈希接口,并为每个密码使用独立盐。简单的 SHA-256 加盐仍然不够,因为攻击者可以高速离线猜测。

总结

安全场景使用 secrets 而不是 random,根据用途选择字节、十六进制或 URL 安全编码。令牌需要足够长度、短期有效、一次性使用、可撤销并通过 HTTPS 传输;比较敏感值时使用 compare_digest()。随机数只是安全设计的一部分,密码哈希、访问控制、频率限制、日志脱敏和生命周期管理同样重要。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐