本文将详细描述实现密码登录中所用到的加密传输、加密存储及设置Cookie的相关知识点。
http的缺点:
中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是一种“间接”的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为“中间人”。
攻击方式:DNS欺骗(通过入侵DNS服务器、控制路由器等方法)、不可靠的代理服务器等
既然明文传输不安全,我们就使用对称加密的方式进行加密传输。对称加密即使用相同的密钥S加密和解密。
但是,密钥如果固定,客户端一旦泄漏密钥,则能用密钥解密。因此需要生成随机密钥。但怎么才能安全地告知对方密钥呢?
如果由服务器端直接告诉客户端该使用哪个密钥,密钥容易泄露,不能保证安全
如何对协商过程进行加密?
密码学领域中,有一种称为“非对称加密”的加密算法
特点是私钥加密后的密文,只要是公钥,都可以解密,但是公钥加密后的密文,只有私钥可以解密。私钥只有一个人有,而公钥可以发给所有的人。
客户端用公钥对密钥S进行加密,发送给服务器。服务器收到后用私钥解密,得到密钥S。这样,客户端就能成功把密钥告知服务端了。因此,使用非对称加密算法进行对称加密算法协商过程,就能保证协商过程的安全了。
为什么不能直接使用非对称加密,而是使用非对称加密算法进行对称加密算法协商过程?因为公钥每个人都知道,服务器用私钥加密,中间人可以用公钥解密获取信息。即只能保证客户端向服务器发送信息安全,不能保证服务器向客户端发送信息安全。
但是问题又来了,客户端要怎么才能得到公钥?
如果由服务端直接向客户端发送公钥,公钥有可能被中间人调包。
但让每个客户端的每个浏览器默认保存所有网站的公钥也是不现实的。
因此,我们利用第三方机构(CA,证书授权中心)颁发数字证书。
CA使用它的私钥对我们的公钥进行加密后,再传给客户端。客户端再使用第三方机构的公钥进行解密。这样,浏览器只需要默认保存1个CA公钥(实际上可能还保存其他可信机构的公钥)就可以了。
但是,CA不可能只给你一家公司制作证书,它也可能会给中间人这样有坏心思的公司发放证书。这样的,中间人就有机会对你的证书进行调包,客户端在这种情况下是无法分辨出是接收的是你的证书,还是中间人的。因为不论中间人,还是你的证书,都能使用CA的公钥进行解密。
那么,客户端要如何才能正确鉴别对方的身份呢?
客户端可以通过验证证书的数字签名来鉴别证书的来源。
先用hash函数生成证书内容的摘要(digest),使用私钥,对这个摘要加密,生成“数字签名”(signature)。
客户端拿到证书后,用CA公钥解密,根据证书上的方法自己生成一个摘要,如果生成的摘要与解密证书上的数字签名得到的摘要相同,那么说明这个证书是真实的,没有被篡改。然后,查看证书上的网站信息,如果与目前浏览的网站一致,则说明证书的来源正确,从而正确鉴别对方的身份。
https = http + TLS/SSL
HTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层TLS。
上面讲的让客户端与服务器端安全地协商出一个对称加密算法。这就是HTTPS中的TLS协议主要干的活。如下图:
似乎无懈可击?
如果中间人使用CA证书,如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。但如果用户点击继续浏览此网站还是会被攻击成功。
而如果中间人使用自己伪造的证书,同样也会发出警告。如果用户点击信任此证书还是会被攻击成功。所以黑客只要诱导用户安装自己伪造的证书即可,例如使用各种钓鱼的不可描述网站。
可以看出,https也挺安全了,但是对于传输密码还不够安全。
因此我们在使用https协议的同时,再使用RSA(非对称加密)进行加密和解密处理。大致如下:
接下来,直接把密码存入数据库?
千万不要用明文存储密码!
如果用明文存储密码(不管是存在数据库还是日志中),一旦数据泄露,所有用户的密码就毫无保留地暴露在黑客的面前,开头提到的风险就可能发生,那我们费半天劲加密传输密码也失去了意义。我们可以从之前的新闻“GitHub 无意中将一些明文密码记录在内部日志中”得到教训。
经常被大家用来加密的算法有MD5和SHA系列(如SHA1、SHA256、SHA384、SHA512等)
md5('truepassword')
但这样容易被破解。黑客会事先计算大量密码对应的各种哈希算法的哈希值,并把密码及对应的哈希值存入一个表格中(这种表格通常被称为彩虹表),在破解密码时只需要到事先准备的彩虹表里匹配即可。
盐,即一个随机的字符串,往明文密码里加盐就是把明文密码和一个随机的字符串拼接在一起。
我们可以先往明文密码加盐,然后再对加盐之后的密码用哈希算法加密。
const salt = Math.round(Math.random() * 10000).toString()
const encrypted = md5(`truepassword@${salt}`) // 用@分割
虽然加盐的算法能有效应对彩虹表的破解法,但它的安全级别并不高,因为计算哈希值耗时极短,黑客仍然可以用穷举法来破解,只是增加了一些耗时。
这两个算法最大的特点是我们可以通过参数设置重复计算的次数,重复计算的次数越多耗时越长。如果计算一个哈希值需要耗时1秒甚至更多,那么黑客们采用暴利法破解密码将几乎不再可能。破解一个6位纯数字密码需要耗时11.5天,更不要说高安全级别的密码了。
而安全换了的是性能的损失,因为它的复杂性,导致了每次计算的耗时远远大于普通的加盐算法。
下面是使用bcryptjs
的例子:
const bcrypt = require('bcryptjs')
const salt = bcrypt.genSaltSync(10) // rounds决定了加密复杂度
const hash = bcrypt.hashSync('truepassword', salt)
bcrypt加密后的字符串形如:$2a 10 10 10asdjflkaydgigadfahgl.asdfaoygoqhgasldhf,其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;
以上就是设置密码时会用到的加密传输与加密存储密码了。
小结一下,流程如下:
当验证密码时,使用bcryptjs的API将解密得到的明文密码与数据库的密文相比较,即可以验证密码。
当验证通过时,就需要生成一个登录态,并写到cookie中。接下来,将详细说这一部分。
由于http协议本身是无状态的,因此协议本身是不支持“登录状态”这样的概念的,必须由项目自己来实现。
那么,如何识别用户的登录态?
session,指代多个有关联的http请求所构成的一个会话。登陆后服务端将sessionid(一般是用户id)设置到Cookie中。这个会话里的每一次请求都带上这个Cookie,服务端通过Cookie既可识别用户。
把用户id设置到Cookie中,再通过用户id获取用户信息,比直接把所有用户信息设置到Cookie中更安全
为什么不直接把用户id设置到cookie中?
因为Cookie携带在HTTP头部中,可以被中间人获取,敏感信息不应该通过Cookie传输。明文的用户id容易被黑客猜测到用户id的生成逻辑(一般是按顺序排列的数字),从而假冒用户。
因此,考虑到sessionid的唯一性,使用uuid生成sessionid。并且,把sessionid与userid的对应关系存入到kv数据库(如redis)中。注意设置过期时间,过期时间应与Cookie一致。
Expires
)或有效期(Max-Age
)Expires
)或有效期(Max-Age
)。Secure
和HttpOnly
Secure
的Cookie只应通过被HTTPS协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过Cookie传输,因为Cookie有其固有的不安全性,Secure 标记也无法提供确实的安全保障。从 Chrome 52 和 Firefox 52 开始,不安全的站点(http:)无法使用Cookie的 Secure 标记。HttpOnly
标记的Cookie,它们只应该发送给服务端。Domain
和 Path
Domain
和 Path
标识定义了Cookie的作用域:即Cookie应该发送给哪些URL。Domain
标识指定了哪些主机可以接受Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了Domain
,则一般包含子域名。Path
标识指定了主机下的哪些路径可以接受Cookie(该URL路径必须存在于请求URL中)。以字符 %x2F
("/") 作为路径分隔符,子路径也会被匹配。SameSite
SameSite
Cookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。但目前SameSite Cookie还处于实验阶段,并不是所有浏览器都支持。详细看HTTP Cookies