px普贤教程PuXian
普贤教程 › 生态包

认证与安全

密码、令牌、验证码、签名——这 12 个包覆盖了一个登录系统需要的全部密码学动作,而且全是纯函数,不需要 OpenSSL。本页所有示例输出都是本机编译产物跑出来的。

先选对密码哈希

包算法适用cost=默认时耗时
bcryptbcrypt($2a$)用户密码,抗 GPUcost 4 → 246 ms(生产用 10–12)
passhashPBKDF2-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) 用的是拒绝采样,分布严格均匀,没有取模偏差。

其余认证相关包

包干什么典型用法
jwtHS256 令牌jwt_encode(payload, secret)
totpHOTP / TOTPotp_totp(secret_b32, 6, now_sec)
oauth2OAuth 2.0 取令牌oa_token_cc(url, id, secret, scope) 等
x509证书解析x509_parse_pem(pem)
captchaASCII 验证码cp_generate(4)
pwgen口令生成pw_generate / pw_generate_pin / pw_generate_passphrase
strength口令强度评估st_score(pw) / st_entropy(pw)
luhnLuhn 校验位银行卡、信用卡号格式校验
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,不要「解出来比对」。