安全的登录系统
为何不安全
当用户在前端输入密码账号后,要把信息发往服务端完成注册/登录,数据需要经过互联网的“黑暗森林”。
在互联网中, CSRF(跨站伪造请求伪造)、XSS(跨站脚本攻击)、MITM(中间人攻击)、RA(请求重放攻击) 等各种不怀好意的程序可在某处窥伺着这份数据。
然后不少刚入门的开发者还在把自己的登录用明文传输!
现在,让我们来谈一谈,如何从明文传输不断改进,打造一个高安全系数的登录系统。
君の安全基本技巧本当上手
1. 摘要
1.1 什么是摘要
简单理解:摘要就是 Hash,也就是把 A 单向映射 B,所谓的单向就是指 B 无法逆向推导出 A。
一个简单的代码例子:
function myHash(value1) {let value2 = 0;for (let i = 0; i <= value1; i++) {value2 += i;} return value2 % 100; // 为了简化,取模100
}
const A1 = 100;
const B1 = myHash(A1); // 50const A2 = 99;
const B2 = myHash(B2); // 50
如果知道 A 的值,那么这里可以推导出 B 的值;
但是如果只知道 B 的值,那么是没有办法知道 A 具体的值的,比如这里 B 是 50,我们也不知道它对应的 A 到底是 99 还是 100 或者别的数。
我们常常说的什么 MD5
, 冲突散列表
base64
、哈夫曼编码
之类的,其实都是一种摘要算法。
1.2 摘要的作用
摘要有一个特点就是,输入哪怕只变动一点,输出也可能会有巨大的变化,让人找不到规律。
这样就可以也就可以防止数据被篡改,保证其完整性。
具体到登录场景,前端可以先用 base64
把数据 data
摘要得到一个 hash1
,再把两者一起发给后端。
后端拿到数据后,也把 data
用 base64
摘要,得到 hash2
,再对比 hash1
和 hash2
,如果一样,就说明 data
没有被篡改过。
这时候,有人就会说了——“这也没用啊,人家都能改你 data 了,那直接把你 hash 一起改了不就完了”
诶,好东西总是成套的,要想更安全,还得搭配其他技巧凑成丝滑小连招。
2. 密钥加密
2.1 对称加密 与 非对称加密
加密就像是一把锁。
- 对称加密: 同一把钥匙,能上锁加密,也能解锁解密。
- 非对称加密:两种钥匙,一种钥匙只能开锁,另一种钥匙只能解锁。
这里的钥匙,都叫做密钥
一个冷知识是,这里的 “钥” 的官方读法是 “yue”。
但是个人还是习惯读作密 “yao” ,因为读作 “蜜月” 总感觉怪怪的。
对称和非对称这两个概念的比喻已经非常形象了,所以我认为也无需过多解释。
我们接着来说说非对称加密种的两种钥匙:
2.2 公钥 与 私钥
其实也是非常所见即所得的概念:
公钥:可以公开的钥匙就是公钥,发给大家用的。
私钥:只有自己或者少数人可以有的就是私钥,是不可以公开的。
那么,非对称加密种,究竟是【加密密钥】作为公钥呢,还是【解密密钥】做公钥呢(另一种钥匙则为私钥)?答案是,两种情况都可以
这里我们进一步介绍下两种情况:
2.4 私钥加密-公钥解密:验证私钥拥有者的身份
这就是说,全世界都可以解密某个密文,但是只有一个人可以产生这种密文——这就实现了(私钥拥有者的)身份验证。
有什么用呢?如果你用过 Github 的 SSH,那就会恍然大悟了——你只需要生成一个密钥对,用其中的私钥来给你的计算机生成一个密文,然后把公钥交给 Github 之类的网站,这些网站就能通过上面说的那样来验证你的身份了。
2.3 公钥加密-私钥解密:构建安全信息通道
这意味着,所有人都能加密数据,而只有少数群体能解密数据,也就是说,数据只有在拥有解密私钥的人手里才是真正有效的——这样一来,从公钥群体传输到私钥群体这个路径,就是一条所谓的安全信道(安全信息通道)了。
我们的登录场景就可以使用这个模式,把用户的数据 data
先摘要得到 hash
, 保证它是完整的,同时也避免了明文传输,再把.hash
用服务端的公钥加密得到密文sec_data
,然后再发送。
这样一来,就算是被恶意程序获取到了我们的数据,它们也没法修改、解读我们的代码了,真是十分美好呢!
但是,这样就真的安全了吗?万一我们的数据被中间人拦截了呢?
2.4 中间人攻击 & 请求重放攻击 & 撞库攻击
中间人可以横在前端与服务端之间,然后再根据自己的心情做事:
- 直接攻击:把请求干掉,或者篡改掉(当然,这里我们已经初步防止了这种问题)。
- 非法收集数据:悄无声息地把每一份经过的数据复制一份,或者原封不动地把请求继续发给服务端。
当然,非法收集数据的不一定是中间人…安全技术任重道远…
对于非法收集数据这一点,有小伙伴就会问了——”我们不是把数据加密过了吗?上一章节才说了安全信道,它拿到的都是密文,是不是就没用呢?“
当然不是的,中间人如果把我们的登录请求原封不动复制一份,那么即使它不知道密码,它也可以直接使用这个请求完成非法用户登录,这就是请求重放攻击。
除此之外,如果它收集了足够多的数据,而我们的加密策略又是多年不变的话,那么经过不法分子们的分析,加密算法就可能被部分破解,随之而来的可能就是超多脚本自动疯狂尝试不同的密码进行登录,这就是撞库攻击,轻则少量用户密码泄露,重则整个加密规则被破解,最后在某个不知名的灰色地带,就会产生一张彩虹表,详细记录着所有用户的密码信息…
问题很多,但是我们不要急,我们一个一个来解决——这里根据我们分析,中间人攻击会衍生出更多问题,它的危害较大,所以优先处理:
2.5 CA 证书:活学活用密钥加密
对于中间人攻击,我们应该思考的第一个问题是,为什么会出现中间人?说到底还是没验证下游的身份,就稀里糊涂把数据交了出去。
那怎么验证身份呢?还记得我们上面说的私钥加密-公钥解密么?其实也很好办,就是每次交出数据前,让下游给我们一个密文,我们常使用之前约定好的公钥尝试解密它,如果能得到有效信息,那说明下游服务就是货真价实的。
但是,如果我们更阴谋论一点想一想,“你怎么证明你自己是你自己”——万一最开始的时候,这个公钥就是伪造的呢?万一最开始和我做约定的,就是中间人呢?
灵魂拷问:你如何证明你是你自己?
如果没法自证清白,那就得请出权威机构来鉴定一下了——这就是CA 机构。
按照现代社会的网络规范来说,每个服务器都得向 CA 机构去申请认证,然后得到一个 CA证书,这个申请过程是这样的:
生成密钥对:服务器首先生成一对公钥和私钥。
申请证书:服务器向 CA 申请证书,提交自己的公钥和身份信息,CA 会对这些信息展开调查验证。
产生 CA 证书:CA 先对【服务器的公钥和身份信息】进行摘要,然后对摘要的得到的 hash 用【CA自己的的私钥】进行加密,这种【摘要+密钥加密】的操作就叫签名。(得到的产物也叫签名,就是一个动词和名词的区别)
再具体一点就是这样的:
// 生成摘要
输入数据: "公钥: ABCD1234, 身份信息: example.com,颁发 CA 机构:xxx, ....."
哈希函数: SHA-256
摘要: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855// 生成签名
私钥: CA的私钥
摘要: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
签名: 1f4e6a7b8c9d0e1f2e3d4c5b6a7b8c9d0e1f2e3d4c5b6a7b8c9d0e1f2e3d4c5b
这个证书是可以给所有人访问的,包含了三个部分:
- 服务器的公钥:因为服务器无法证明“自己是自己”,所以公钥和私钥都由CA 官方来它发一个,(当然,私钥是不会出现在CA证书上的,而是由服务器自己保管好)
- 服务器信息:介绍了服务器的一些基本情况,以及数字签名用到的一些摘要算法
- 数字签名:一个字符串,产生的方式是:1. 将上面两部分作为参数进行摘要得到一个
hash
,保证数据不可被篡改。2. 然后再用 【CA 给的私钥】 加密,生成一个字符串,也就是所谓的数字签名了。
有了这个机制,如果我们默认 CA 机构足够权威可信,那么我们访问下游服务的时候就会先让它出示 CA 证书,然后我们需要判断证书的真伪:
- 用户先从某处(下文会提到)获取**【CA的公钥】**——这是计算机出厂后装系统时自动内置的,然后用它把数字签名给解密,还原到摘要
hash
- 然后阅读CA机构鉴定过的【服务器信息】,得到其中使用的摘要算法,用这个算法把【服务器公钥】和【服务器信息】作为参数进行摘要,得到
hash1
,对比hash1
和hash
,如果一致,就说明数据没被篡改过。
这样一来,有了 CA 的认证,服务器就可以排除嫌疑了——等等,万一这个 CA 机构是假的呢?
这里就得解释上文中提到的某处是什么了:
- 计算机出厂后,安装系统时,会把一些最最最权威的【CA 机构自己的证书(结构跟上面大体一样)】一起内置到系统中,这也就是所谓的某处。当然,浏览器也类似,会内置 CA 机构证书。
- CA 机构 也有三六九等的,【有一点点权威的CA】需要【更有权威的CA】来颁发证书认证(流程跟上面一样),而我们系统/浏览器内置的都是【最大权威】的——这样的关系就叫做,CA信任链。
- 当我们从服务器拿到一个野生证书的时候,就会根据证书中的【服务器信息】来逐步向上查找它的最终颁发机构,如果这个机构是我们系统/浏览器中内置的【最大权威】的机构之一的话,就说明它是真的。
绕了这么多弯,我们终于能够证明下游服务是真实可信的了。——而这一切的基础是 我们始终相信【最大权威】的 CA 是可信的,
不然,几乎无法建立完全可信的网络体系(或许区块链能够带来一些新的可能?)。
就这样,绝大部分中间人攻击被避免了(但是网络安全就没有绝对的事情)。
2.6 防碰撞随机数 与 加盐
为了应对重放攻击,我们会在登录请求前,先额外提前请求让服务端生成一个一次性随机数,之后登录请求时,在某处带上这个随机数交给服务端,服务端会验证随机数是否有效,如果有效的话就核销这个随机数让它不可再使用第二次。
- 在这个场景下,我们把一次性随机数作为防碰撞随机数使用,但是二者并不是完全一样的概念(防碰撞不一定要用一次性随机数)
- 当然这里可能又有同学要问了,万一下发这个随机数的时候又被中间人拦截了——我们在上一章已经解决了中间人问题,所以这里不需要有这种担心。
- 我们这里讨论的也是【通过非中间人的手段获取到登录请求】进而发动的重放攻击——比如在用户本地注入了恶意脚本,在其浏览器中抓包。不过这也是非常尴尬的一点,这些恶意脚本通常都能够发动更直接的攻击,而不需要再使用重放攻击了——我们这里就当作是为了学习知识而强行说有这种重放攻击的可能吧。
这里的某处有很多选择,比如可以是 http header 中带上一个 token,也可以是在用服务端的公钥将密码hash
加密时,顺带把随机数也作为参数加密。
具体怎么做,具体场景需要具体分析
最后,在落库前,我们需要把 hash
值加盐。
加盐是指,在原始数据的固定位置上加入随机内容。
同过加盐,我们使得最终的输出的结果出现差异
这样就避免了多个用户都用一个密码导致hash
值一样的情况,见笑了撞库的可能。
再等等,课本里面告诉我们 HTTPS 是能有效避免中间人攻击的,但这里怎怎么只说 CA 一点没提到 HTTPS 呢?原来,摘要,密钥加密,包括 CA 证书,防碰撞随机数,还有加盐,都是 HTTPS 的一环。
2.6 HTTPS: 上述章节都只是我的其中一环
既然都说到了 HTTPS 了,那不妨顺带把 HTTP 一起回顾了,而既然都说到了 HTTP 了,那干脆从 TCP 三次握手讲起:
TODO: 这部分后面回头补上
知识加餐
如果你对 Rust、Web、服务等知识点感兴趣,也欢迎看看我的其他文章,这里只推荐干货,绝不啰嗦:
Docker快速上手:【安装/配置/使用/原理】一条龙教程
Rust 字符串(可变字符串String与字符串切片&str)
Rust 智能指针