3、密码认证机制:静态密码、动态密码、密码策略与复杂度要求、密码存储安全(哈希加盐)
密码认证,说白了就是「你知道什么」的验证方式。你输入一串字符,系统核对一下,对了就放你进去。听起来简单,但这里面的坑,我踩过不少。
咱们这一章,就把密码认证的四个核心维度拆开揉碎了讲。静态密码怎么定?动态密码怎么用?策略怎么设才不让人骂娘?存储怎么搞才不会被一锅端?
3.1 静态密码:最古老也最头疼的认证方式
静态密码,就是固定不变的密码。你设一个,用到下次修改为止。我见过太多系统,用户密码用了五年没换过,还全是「123456」这种级别的。
静态密码最大的问题是什么?可预测性和复用性。用户喜欢用生日、手机号、宠物名字,而且一个密码走天下。你系统做得再安全,用户自己在别的网站泄露了密码,你的系统也跟着遭殃。
核心要点:静态密码本身不是问题,问题是用户行为和系统策略太弱。我建议把静态密码当作「基础防线」,而不是唯一防线。
3.2 动态密码:给静态密码加个「一次性」buff
动态密码,也叫一次性密码(OTP)。每次登录都生成一个新密码,用完就废。这玩意儿能有效对抗重放攻击和凭证窃取。
常见的动态密码实现方式有两种:
- TOTP(基于时间的一次性密码):每30秒或60秒生成一个新密码。Google Authenticator、Authy 都是这个路子。
- HOTP(基于计数器的一次性密码):基于一个递增的计数器生成密码。你用一次,计数器加一,下次密码就变了。
我记得有一次给一个金融客户做安全审计,他们只用静态密码登录后台。我说:「你们这后台能直接操作转账,连个动态密码都不上?」后来他们加了 TOTP,再也没出过凭证泄露导致的问题。
我的建议:动态密码不要用短信发送。短信有被拦截的风险(SS7攻击了解一下)。优先用 TOTP 应用或硬件令牌。
3.3 密码策略与复杂度要求:别让用户恨你,也别让黑客笑你
密码策略,是系统对用户设密码的约束。复杂度要求,是这些约束的具体指标。这里有个平衡问题:策略太严,用户记不住,就写在便签纸上贴显示器上;策略太松,黑客一猜就中。
我见过最离谱的策略是什么?要求密码必须包含大小写字母、数字、特殊字符,长度至少16位,而且每30天强制更换。结果呢?用户全在密码后面加个「!01」、「!02」这样递增。这跟没换有什么区别?
现在业界更推荐的做法是:
- 长度优先:至少12位,越长越好。长度比复杂度更重要。
- 弱化强制更换:除非有泄露证据,否则不要强迫用户每三个月换一次密码。
- 禁止常见密码:维护一个黑名单,把「password」、「123456」、「qwerty」这些直接拦住。
- 支持密码管理器:别限制粘贴功能,让用户能用密码管理器生成和填充强密码。
| 策略项 | 传统做法(不推荐) | 现代做法(推荐) |
|---|---|---|
| 最小长度 | 8位 | 12-16位 |
| 复杂度要求 | 必须包含大小写+数字+特殊字符 | 鼓励使用长短语(passphrase) |
| 更换周期 | 30-90天强制更换 | 仅在泄露时要求更换 |
| 历史密码 | 禁止使用最近5次密码 | 禁止使用最近1-2次密码 |
| 常见密码检查 | 无 | 必须检查黑名单 |
注意:我曾经见过一个系统,密码策略要求「必须包含至少一个大写字母」,结果用户全在首字母大写。这种策略形同虚设。策略要设计得让用户「不得不」设一个强密码,而不是「不得不」设一个符合规则但依然很弱的密码。
3.4 密码存储安全:哈希加盐,这是底线
密码怎么存?直接存明文?那是找死。加密存储?密钥泄露就全完。正确的做法是:哈希加盐。
哈希,是一种单向函数。你把密码扔进去,出来一串固定长度的摘要。这个过程不可逆。你没法从摘要反推出原始密码。
盐,是一段随机字符串。每个用户的盐都不一样。你把盐和密码拼在一起,再哈希。这样就算两个用户设了相同的密码,存下来的哈希值也不一样。
为什么一定要加盐?不加盐会怎样?
- 彩虹表攻击:攻击者预计算好常见密码的哈希值,直接查表就能匹配。加盐之后,彩虹表就废了,因为每个盐值都要重新算一遍。
- 相同密码暴露:不加盐的话,两个用户密码相同,哈希值也相同。攻击者一看就知道这俩人密码一样。加盐之后,哈希值完全不同。
我给你们看一段伪代码,演示一下正确的哈希加盐流程:
// 注册时
salt = generateRandomString(16) // 生成16字节随机盐
hash = bcrypt(password + salt) // 用bcrypt做哈希
store(username, hash, salt) // 存用户名、哈希值、盐
// 登录时
salt = getSaltFromDB(username) // 从数据库取出盐
hash = bcrypt(inputPassword + salt) // 用同样的盐哈希用户输入的密码
storedHash = getHashFromDB(username) // 取出存储的哈希值
if hash == storedHash:
loginSuccess()
else:
loginFailed()
这里我用了 bcrypt。为什么是 bcrypt?因为它自带盐,而且可以调节计算强度。你算一次要100毫秒,用户登录慢一点无所谓,但攻击者想暴力破解?算一个密码就要100毫秒,算一亿个?那得算到猴年马月去。
推荐算法优先级:bcrypt > scrypt > argon2 > PBKDF2。别用 MD5,别用 SHA-1,别用明文。这些我都在生产环境里见过,每次看到都头皮发麻。
3.5 密码认证的整体流程
我把整个密码认证的流程画了一张图,你们看看各个环节是怎么串起来的:
这张图把注册和登录两条线分开了。注册时,密码先过策略检查,再哈希加盐存库。登录时,先查盐,再算哈希,最后比对。比对结果决定你进不进门。
3.6 避坑指南:我踩过的那些坑
做密码认证这么多年,我踩过的坑不少。分享几个给你们:
- 坑一:自己写哈希算法。 我曾经见过一个团队自己实现了一个「加密算法」,其实就是把密码每个字符的ASCII码加了一个固定值。这跟明文没区别。永远用标准库,永远。
- 坑二:盐值复用。 所有用户用同一个盐,或者盐值太短(比如4位)。这跟没加盐差不多。每个用户独立生成16字节以上的随机盐。
- 坑三:前端哈希。 有人觉得「我在前端先哈希一次,后端再哈希一次,双重保险」。错。前端哈希只防传输过程中的明文泄露,后端该怎么做还是怎么做。而且前端哈希用的算法通常太弱(MD5、SHA-1),反而降低了安全性。
- 坑四:密码长度不限制。 不限制长度,攻击者可以传一个超长字符串导致哈希计算耗时暴增,搞DoS攻击。我建议限制在128位以内。
一个小技巧:如果你用 bcrypt,计算成本参数(cost factor)设到10-12比较合适。太低不安全,太高用户登录要等半天。我在生产环境里一般用12,用户感知不到延迟,但暴力破解的成本已经很高了。
密码认证,说难不难,说简单也不简单。静态密码、动态密码、策略、存储,这四个环节任何一个出问题,整个认证体系就形同虚设。你想想看,你花了几百万做的业务系统,结果因为密码存的是明文被拖库了,那得多冤?
嗯,这一章的内容就到这儿。记住:密码认证的底线是哈希加盐,上限是多因素认证。 别在底线上省钱,也别在上限上偷懒。