普贤教程 › 生态包
认证与安全
密码、令牌、验证码、签名——这 12 个包覆盖了一个登录系统需要的全部密码学动作,而且全是纯函数,不需要 OpenSSL。本页所有示例输出都是本机编译产物跑出来的。
先选对密码哈希
| 包 | 算法 | 适用 | cost=默认时耗时 |
|---|---|---|---|
bcrypt | bcrypt($2a$) | 用户密码,抗 GPU | cost 4 → 246 ms(生产用 10–12) |
passhash | PBKDF2-HMAC-SHA256 | 需要自解释存储串的场景 | 10,000 次迭代 |
bcrypt
px
import bcrypt def main(): var t0 = now_ms() var h = bc_hash_with("P@ssw0rd", 4).ok() # cost 4 只用于演示 print("耗时 ms => " + str(now_ms() - t0)) print(h) print(bc_verify("P@ssw0rd", h).ok()) # true print(bc_verify("wrong", h).ok()) # false print(bc_info(h).ok()["cost"]) # 4
text
耗时 ms => 246 $2a$04$ifZjz3..tslZQ975egtoHO4N1rE30Xw9gsrx.SCQMDfzBaTx/b4nC true false 4
cost 每 +1,耗时翻倍cost 4 = 246 ms 在解释轨测的,cost 10 会是 2^6 倍。生产建议 cost 10–12(单次 0.1–0.3 秒,登录接口可接受),并配合
bc_needs_rehash(hash, target_cost) 在用户下次登录时升级。PBKDF2:存储串自带参数
px
import passhash var p = pass_hash("P@ssw0rd").ok() print(p) # pbkdf2$10000$3029a59ff7e56...$... print(pass_verify("P@ssw0rd", p)) # true print(pass_verify("bad", p)) # false
格式 pbkdf2$<迭代数>$<salt hex>$<hash hex>:迭代数、盐都在串里,将来调参不用迁移数据。
验证码
px
import captcha var c = cp_generate(4).ok() print(c["answer"]) # 7672 print(cp_valid(c["answer"])) # true print(len(split(c["image"], "\n"))) # 5 —— 5 行 ASCII print(split(c["image"], "\n")[0]) # _ _ _ _
刻意做成 ASCII(3×5 七段字体),好处是不依赖字体文件、不依赖图形库,服务端渲染零成本;代价是 OCR 很好读——所以它的价值不在「防机器」,而在抬高自动化成本,真防线是限流与一次性。
四个必备动作:
- 一次性:校验通过立即删掉服务端记录。
- 短时效:3 分钟过期。
- 错 3 次作废:防爆破。
- 签发太快判可疑:签发到提交 < 1.2 秒,人不可能这么快。
TOTP(两步验证)
px
import totp var sec = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" # RFC 6238 测试密钥 print(otp_totp(sec, 8, 59).ok()) # 94287082 ← RFC 测试向量 print(otp_totp(sec, 8, 1111111109).ok()) # 07081804 ← RFC 测试向量 print(otp_verify(sec, code, 6, now_sec, 1)) # ±1 个 30 秒步长
两条测试向量都对上了说明 HMAC-SHA1 + 动态截断 + 取模补零整条链实现正确。自己写 TOTP 最容易错的是「步长」(30 秒) 与「补零」(digits 位),这里都被测试向量锁住了。
otp_verify 的 window 是容错步长:给 1 表示前后各 30 秒都收。别给太大,否则等于把有效期从 30 秒放宽到几分钟。
随机与口令
px
import secure_random import pwgen print(sr_hex(8).ok()) # 170936c515abc94c print(sr_int(100).ok()) # 1 print(sr_token(16).ok()) # xkM-N1MSQL3B9zQYyR99PQ (base64url,无 padding) print(sr_choice(["a","b","c"]).ok()) print(pw_generate({"length": 16}).ok()) print(pw_generate_pin(6).ok()) # 纯数字 PIN print(pw_generate_passphrase(4).ok()) # 四词口令
别用
random 造口令sr_* 走 os_random_hex(内核 CSPRNG);sr_int(max) 用的是拒绝采样,分布严格均匀,没有取模偏差。其余认证相关包
| 包 | 干什么 | 典型用法 |
|---|---|---|
jwt | HS256 令牌 | jwt_encode(payload, secret) |
totp | HOTP / TOTP | otp_totp(secret_b32, 6, now_sec) |
oauth2 | OAuth 2.0 取令牌 | oa_token_cc(url, id, secret, scope) 等 |
x509 | 证书解析 | x509_parse_pem(pem) |
captcha | ASCII 验证码 | cp_generate(4) |
pwgen | 口令生成 | pw_generate / pw_generate_pin / pw_generate_passphrase |
strength | 口令强度评估 | st_score(pw) / st_entropy(pw) |
luhn | Luhn 校验位 | 银行卡、信用卡号格式校验 |
idcard | 身份证校验 | 校验码 + 出生日期 + 性别 |
validator | 字段规则校验 | val_validate(data, rules) |
base58 / checksum | 编码与校验和 | 短 ID、CRC32 |
身份证校验(GB 11643-1999)
px
import idcard print(idcard_validate("110101199003078515")) # Ok({birth: 19900307, city_name: 北京市, gender: male, province: 11, province_name: 北京, type: 18}) print(idcard_validate("110101199003078514").err()) # idcard: 校验码不符(期望 5)
它做的是完整校验:地区码、闰年、校验码加权、性别(第 17 位奇男偶女)。15 位旧号用 idcard_upgrade15to18 升位。
字段校验:一次报全部错误
px
import validator print(val_validate({"email": "a@b.com", "age": 20}, {"email": "required|email", "age": "int_range=18,60"})) # Ok(null) print(val_validate({"email": "bad", "phone": "123"}, {"email": "email", "phone": "phone"}).err()) # email: 邮箱格式错误; phone: 手机号格式错误
规则用 | 分隔或写成列表:required / email / url / phone / min=N / max=N / length=A,B / int_range=A,B / in=a,b,c。
工程纪律
- 密钥不落源码:
env("JWT_SECRET"),别写常量。 - 日志不落明文:密码、验证码、token 一律不进日志。
- 不该自己写密码学:这里是「用库」,不是「实现算法」。要加算法就提给语言维护方,不要在生产里手搓 S-box。
- 哈希不可逆:验证永远用
verify,不要「解出来比对」。