现实世界是复杂的,因此即使是一个简单的系统,面对线上环境时也不得不力求考虑周到。 在安全方面的经验是:不考虑,必出事。

本文总结了一些设计注册登录流程时所必须的,当前较好的安全实践。 包括传输过程,前后端处理,以及相关算法的选择等。

场景简述

一个最基础的用户认证系统,应包含至少三个部分:注册,登录和验证。

用户通过 账户 + 密码 来注册登录,登录验证成功后下发一个按规则过期的 token。后续的请求均携带该 token,在过期之前视为已认证用户,可进行相关权限的操作。业务接口在调用前统一拦截检查,如发现请求未认证,则重定向至注册登录页面。

以上就是一个典型的,完整可用的流程。实际情况中一般还会有修改密码,重置密码,第三方登录等部分,后续提到的设计对它们同样有效。

问题分类

安全问题是一个全局性的问题,符合木桶短板理论,整个链路上最薄弱的环节决定了系统整体的强度。

从大体流程上看,涉及到的部分有:

  • 前端:用户输入账户密码,发起请求。
  • 传输过程:数据从前端发送到服务端的过程。
  • 服务端:进行验证,保存和更新数据,

从认证逻辑上看,安全性的关键点在于:

  • 如何保障数据不泄露。
  • 如何保障数据完整,不被篡改。

能在整个流程上覆盖到所有关键点,安全问题就基本有保障了。

具体设计

流程安全

已经有比较成熟的规范,日常使用的各大产品上也都有应用:

前端

  • 输入密码时用掩码替代,如点或星号。
  • 尽可能使用安全的输入框,阻止复制或缓存。

传输过程

  • 使用 HTTPS。
  • 敏感信息进行加密,不要明文传输。

服务端

  • 登录失败时不返回具体原因,防止对照猜解。
  • 限制设备的登录尝试频率,拒绝异常请求。
  • 限制一定时间内账号登录失败的次数,防止暴力破解。

整个链路中只有服务端是完全可控和可信的,而前端应该默认为可以被看到和修改代码,即不可信的。

数据安全

不泄露

只在安全信道传输。 一定要用 HTTPS,不然数据就在网上裸奔了,可以随便被拦截。虽然也能被中间人攻击,但至少发生在用户侧,应该属于用户可知的范围。

敏感信息必须加密。 不论是本地保存密码还是发送登录请求,只要是敏感信息,一律不能明文储存和传输。尽可能防止明文被中间人看到,或者被打印在某处 log 中。

服务端不能保存密码明文。 老生常谈了,只保存一个处理后的哈希值,只能单向验证提交的密码是否正确,无法反推出原始密码明文。需要强调的是,简单的做一层 MD5 摘要已经落后了,应该考虑加盐或使用更安全的算法。

完整,不被篡改

对请求内容全文做签名。 如果对接过支付系统,对验签这个环节肯定不会陌生。服务端收到请求后,必须先验证签名,确保请求的完整和真实性,才能继续做业务处理。

算法选择

前端加密

网上有很多人认为没必要,个人觉得意义还是有的,不过仅在于防止储存和传输中明文泄露,因此只要不那么容易被发现和解密出来即可,常用的加密算法都满足。

密码哈希

这一块说起来比较复杂,细节可以单独写一篇文章了,主要是密码学的内容。简单来说,使用公认的最新推荐的实践方法,千万不要自行设计加密方式,也不要只做一次简单的哈希。多种散列算法组合使用不一定能提高安全性,加盐如果是公开的盐或一致的盐,效果也很有限。

总之一句话:遵照密钥派生函数的最新实践,不要自己瞎折腾。

当前比较推荐的有:Scrypt,Bcrypt 和 Argon2。

总结

关于用户认证系统的安全,每个部分都有很多细节值得考虑,此处的讨论仅仅是一小部分。

遵守好的规范不一定就能完全避免安全问题,但至少可以很大的降低风险。没有绝对的安全,但当成本远高于收益时,就会有概率上大范围的安全。

最后提供一个综合的设计列表,以供参考:

  • 只通过 HTTPS 传输,保障信道安全。
  • 请求均带有时间戳和随机数,进行 AES 加密,防重放与中间人攻击。
  • 限制时间段内的登录尝试次数,防止找规律。
  • 验证时按固定时间返回结果,防止被按字节猜解。
  • 验证失败时只返回【用户名或密码错误】,防止暴力猜解。
  • 提交密码时前端强制验证用户的密码强度,阻止使用过于简单的密码。
  • 服务端只保存经过 argon2 等高强度算法处理后的 Hash 值。
  • 每次更新密码均会生成安全随机的新盐,保证密码 Hash 的独立安全性。