账号系统安全

2016-03-31 fishedee 后端

1 概述

我们探讨下怎么设计一个安全的账号系统,涉及到注册与登录的两方面内容

2 注册

2.1 流程

一个最简陋的账号注册流程是

  1. 用户输入账号名与密码
  2. 服务器收到后保存到数据库

这里涉及到两个问题,密码如何传输,密码如何保存

2.2 密码传输

安全的密码传输保证了传输过程中密码数据不会泄漏

2.2.1 明文传输

好吧,明文传输的话在你家的路由器抓包一下就能知道你的注册密码了

2.2.2 散列传输

先将密码进行sha512哈希,然后密码哈希后传递给后台。这种办法一样很愚蠢,路由器抓包后,直接用sha512后的密码再次请求后台就能登录成功。这样做唯一的价值即使用户的md5密码泄露了,它的原密码仍然是可靠的。这里的sha512是目前来说非常可靠的散列算法了,它相当于md5,与sha1以后的加强版。看这里

2.2.3 对称加密传输

启动后,服务器返回随机生成数key,客户端以key和密码在des加密,服务器获取后用这个key做des解密就可以了。嗯,这种办法也是一样的忧伤,因为加密的key在线上传递的,路由器获取数据后,密文数据和key数据后,就可以做一次本地解密了。破解难度稍大,但仍然不难。

2.2.4 非对称加密传输

启动后,服务器返回RSA的公私钥匙对,客户端以公匙和密码在RSA加密,服务器获取后用私匙做rsa解密就可以了。由于非对称加密算法保证了公私钥匙对的可靠性,公有钥匙无法推导到私有钥匙,所以路由器抓包是无法破解到用户的密码。但是,这无法阻挡路由器做重放攻击,也叫中间人攻击来窃取密码。不过,这样做的破解难度略高,而且需要提前部署实时破解。目前,仍会有像豆瓣,人人的网站来采取这种传输方式,看这里

2.2.5 https传输

简单暴力的用对注册页面进行https加密,保证全程传输数据无法抓包攻击,无法重放攻击,安全可靠。但是,开了https也不是仍然万能,如果你在允许网站https传输的同时,也开启http传输,这会重新导致中间人攻击。看这里

Strict-Transport-Security: max-age=31536000;

你要做的仅仅是需要在页面中加入这句,以保证网站只支持https传输。路由器无法做中间人http重放攻击。

2.3 密码保存

安全的密码保存保证了即使数据库数据泄漏了,仍然不会对用户密码数据产生泄漏。

2.3.1 明文保存

无法想象是明文传输,这件事已经谈了很多次了。

2.3.2 散列保存

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69afe7dd238f3364946366
hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542

对数据进行散列(md5或sha1)后放入数据库保存。但无法抵御彩虹表破解,只要给出足够的时间,然后计算一些常用的密码的hash值,当获得数据库的密码散列表后,直接反向查询就能获取到原密码了。

md5(sha1(password))
md5(md5(salt) + md5(password))
sha1(sha1(password))
sha1(str_rot13(password + salt))
md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

人们可能解决的将不同的hash函数组合在一起用可以让数据更安全。但实际上,这种方式带来的效果很微小。反而可能带来一些互通性的问题,甚至有时候会让hash更加的不安全。

2.3.2 加salt散列保存

hash("hello")                    = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

查表和彩虹表的方式之所以有效是因为每一个密码的都是通过同样的方式来进行hash的。如果两个用户使用了同样的密码,那么一定他们的密码hash也一定相同。我们可以通过让每一个hash随机化,同一个密码hash两次,得到的不同的hash来避免这种攻击。

具体的操作就是给密码加一个随即的前缀或者后缀,然后再进行hash。这个随即的后缀或者前缀成为“盐”。正如上面给出的例子一样,通过加盐,相同的密码每次hash都是完全不一样的字符串了。检查用户输入的密码是否正确的时候,我们也还需要这个盐,所以盐一般都是跟hash一起保存在数据库里,或者作为hash字符串的一部分。

//盐的长度太短
hash("hello" + "Q") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello2" + "E") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
//盐的重复使用
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello2" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1

另外还有两种常见的加盐错误办法,盐的长度是一位数,对于制作彩虹表的人来说依然没有难度,他最多只需要为每个密码额外增多26倍的工作量。另外,所有用户的盐都是一样的,这跟没有加盐是同一个破解难度,完全消除了加盐的优势。切记,用户每次创建或者修改密码一定要使用一个新的随机的盐,而且盐必须是完全随机,而且足够长的。

加盐抵御了托库导致所有密码泄漏的风险,黑客需要单独为每个用户建立彩虹表。但是,加盐并没有提高单个用户破解的难度。如果黑客拖库后,只想针对性地对某些用户进行密码破解,那么加盐就是形同虚设。

2.3.3 加salt+慢速散列保存

通过设计一个相当复杂的hash算法,来大幅延长彩虹表的破解时间,从而让破解难度指数式增长。这类算法是PBKDF2或者bcrypt算法,算法中可以指定hash的迭代次数,而且保证要计算出这个哈希值,是无法缩短这其中任意一个迭代的。这造成了每次hash的时间即使用最快的机器也大概为500ms一次,如果黑客拖库想建立一个彩虹表,那么仅是尝试5w次密码,就已经达到一天的时间。而一般一个六位的密码(大写字母+小写字母+数字,总的可能性是大概为5^19次,计算这个散列的时间就已经达到了83609个世纪。这已经是天文时间了,更何况你现在还会在哪里输入6位数的密码呢?

但这个方法的缺点是自损三千杀敌一百,慢速哈希会大量消耗cpu时间,这让整个系统的登录校验机器成N倍的增长。如果控制不好的话,更会造成DDos攻击。所以,这个方法一般只会用在非常非常关键的加密业务场景中。

3 登录

3.1 流程

一个最简陋的账号登录流程是

  1. 用户输入账号名与密码
  2. 服务器收到后设置登录态,返回成功

这里涉及到两个问题,密码如何传输(这个上一篇已经说了,不再啰嗦),如何保存登录态

3.2 登录态

3.2.1 cookie明文保存

直接将登录用户的userId信息写在cookie上,这跟密码用明文保存一个道理,点击两下鼠标就能破解了。

3.2.2 随机数登陆态保存

使用一个随机数作为用户的登录态的标志,但关键的登录数据仍然放在服务器,这保证了登录数据不能被用户串改。

sessionId = Math.rand()
sessionId = userId

常见的登录态错误写法是将sessionId设置为伪随机数,或者是userId。这会导致登录态会被随意猜测,从而在获得当前登录态的同时,推断到下一个登录态,实现账号跨权限操作。

3.2.2 随机数登陆态保存+IP校验

仅仅用sessionId作为用户登录态的话,系统的安全性会变得很脆弱。黑客一旦侵入系统获得sessionId后,复制sessionId就能伪造用户登录态了。

设置cookie的Secure为HttpOnly,确保黑客不能用xss等技术来获取登录态,从而增强破解难度。

记录sessionId的IP信息,或userAgent信息,当黑客窃取sessionId后登录将会提醒用户有异地登录,或拒绝登录,从而提高安全性。

相关文章