在安全领域,生成密码或 Token 时,`random` 模块是绝对不能用的。因为 `random` 依赖的是伪随机数生成器(PRNG),种子可预测、输出可重现,压根不具备密码学安全性。而 `secrets` 模块才是正确选择,它提供了 `token_urlsafe()`、`choice()` 和 `compare_digest()` 等安全函数。
先来看一个真实案例:某内部工具用 `random.SystemRandom().choice()` 拼接了一个 8 位字符串作为登录 Token,结果被渗透测试人员通过进程内存快照还原出 PRNG 状态,直接批量生成了有效 Token。这可不是危言耸听,而是真实踩过的坑。
---
为什么不能用 random 生成密码或 Token
`random` 模块用的是伪随机数生成器(PRNG),种子可预测、输出可重现,不适合安全场景。比如用 `random.choice()` 生成的 API Token,如果攻击者知道程序启动时间或部分输出,就可能反推后续所有值。
从底层看:
- `random` 不保证密码学安全性,哪怕调用了 `SystemRandom`,其底层仍依赖 OS 的非加密随机源(如 `/dev/urandom`)——但 `random` 自己没做熵校验和重采样。
- Python 3.6+ 明确推荐用 `secrets` 替代 `random` 做安全敏感操作。
- Web 框架(如 Flask、FastAPI)的 `secret_key` 或 `session_id` 生成逻辑,若手动拼接 `random` 结果,会直接降低整体安全性。
用 secrets.token_urlsafe() 生成 URL 友好 Token
这是最常用也最省心的方式:生成 Base64Url 编码字符串,不含 `+`、`/`、`=`,能直接放进 URL、HTTP Header 或数据库字段。
要注意的是,长度参数是「字节长度」,不是最终字符串长度。Base64Url 编码后长度 ≈ ⌈(n × 4)/3⌉,所以要 32 字符 Token,传 `24` 字节更准:
```python
import secrets
token = secrets.token_urlsafe(24) # 实际生成约 32 个字符
# 示例输出: 'xvL9aKmQ2rFzB4tN7wYpE8sJ'
```
关键要点:
- 默认使用 `/dev/urandom`(Linux/macOS)或 `CryptGenRandom`(Windows),系统级密码学安全随机源。
- 不建议用 `secrets.token_urlsafe(16)` 生成密码——它只提供约 128 位熵,对长期口令不够;密码应至少 16 字节(128 bit)以上。
- 避免在循环里反复调用生成多个 token 后拼接,这不会增加熵,反而可能引入时序侧信道。
用 secrets.choice() + 字符集定制密码
如果需要控制字符类型(比如必须含大小写字母、数字、符号),用 `secrets.choice()` 安全地从自定义字符集中抽样。别用 `random.choice()`,哪怕加了 `SystemRandom` 也不行。
```python
import secrets
import string
alphabet = string.ascii_letters + string.digits + "!@#$%&*"
password = ''.join(secrets.choice(alphabet) for _ in range(16))
```
几个容易忽略的细节:
- 字符集别漏掉引号里的转义字符,比如 `\!@\#$\%` 直接写成 `!@#$%` 就行,Python 字符串本身不解释这些。
- 不要用 `secrets.randbelow(len(alphabet))` 再索引——虽然也安全,但比 `secrets.choice()` 多一次计算,且易写错边界(如 `randbelow(len(alphabet)-1)`)。
- 如果要求“至少一个大写、一个小写、一个数字”,得用拒绝采样法重试,不能靠概率估算后固定位置插入——否则会降低熵值。
secrets.compare_digest() 防止时序攻击
生成完密码或 token 后,校验时别用 `==` 直接比较字符串。攻击者可通过响应时间差异推测出前缀是否匹配。
`secrets.compare_digest()` 是恒定时间比较函数,无论输入是否相等,执行时间都一致:
```python
if secrets.compare_digest(user_input_token, stored_token):
grant_access()
else:
deny_access()
```
几点提醒:
- 仅适用于 bytes 或 str 类型,且两个参数类型必须一致(不能 str vs bytes)。
- Django 和 Flask 的 session 签名验证底层已用类似机制,但你自己写的 token 校验点必须手动加。
- 别试图自己实现恒定时间比较——容易写出被编译器优化掉的“假恒定时间”代码。
真正难的不是选对函数,而是意识到哪些地方需要它:Token 生成只是起点,存储加密、传输 HTTPS、校验方式、过期策略、错误提示粒度……每个环节松动一环,前面用 `secrets` 做的就白费了。

本文转载于:https://www.php.cn/faq/2450362.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。