加密过程
目前较为常用的对称加密算法是 AES 加密。为了实现非对称加密,我们需要使用 RSA 公钥 加密的方式。然而 RSA公钥 加密的速度只有 AES加密 的几百分之一,在加密通讯中一般不使用公钥直接对数据进行加密,虽然我们这里的目的只是对登录的账号密码进行加密,为了使这个过程变得通用化,我们采用 RSA公钥 与 AES 相结合的方式。
在前端发送请求开始,到后端返回响应结果,整个流程可以在上图中体现出来。
1. 首先使用 OpenSSL 生成 RSA 公钥和私钥,后端存放私钥,前端可以通过接口获取公钥,也可以在部署时把公钥打包进去。
2. 前端生成长度为 64 的随机字符串作为 「AES 密钥」,使用公钥对 「AES 密钥」进行加密得到「加密的AES密钥」。
3. 使用 「AES密钥」对将要发送给后端的数据「消息明文」进行加密,得到「消息密文」。
4. 把「加密的AES密钥」和「消息密文」一起发送给后端。
5. 后端收到请求数据后,先使用「RSA私钥」对「加密的AES密钥」解密,得到实际的「AES密钥」。
6. 然后使用「AES密钥」对接收到的「消息密文」进行解密,得到前端发送过来的实际数据,然后进行业务处理,得到将要返回给前端的「响应结果」。
7. 后端使用「AES密钥」对结果数据进行进行加密后再把「加密结果」返回给前端。
8. 前端用「AES密钥」,也就是开始生成的 64 位随机字符串对加密结果进行解密,得到真实的响应数据。
以上就是完整的前后端解密通讯的完整过程。
隐患
在上述过程中存在的隐患就是,我们无法保证公钥的合法性,也就是这个公钥到底是不是服务器真正的公钥。因为 HTTP 协议传输容易受到「中间人攻击」,假设 A 通过一些手段拦截了我们获取证书的请求,在收到服务器返回的证书后将其保存起来,然后在请求中给我们返回 A 自己的公钥。那我们加密使用的就是 A 的公钥,等我们发送数据的时候,A 就可以用自己私钥进行解密得到我们的数据,再用服务器公钥把数据进行加密传输给服务端。整个请求过程没有任何问题,然而我们的数据已经神不知鬼不觉地被窃取了。
要保证公钥的合法性,我们可以使用「数字签名」。数字签名实际上使用的也是「RSA公钥」,只不过它的操作跟加密传输是反过来的。数据传输时我们是客户端使用「公钥加密」,服务端使用「私钥解密」,这样可以确保只有拥有私钥的服务器才能解密数据;而数据签名是使用「私钥加密」,客户端使用「公钥验证」,目的是只有拥有私钥的服务端才能生成这个唯一的「数字签名」,由于公钥是公开的,任何人都可以使用公钥去验证签名的合法性。
也就是说,为了保证公钥的合法性,需要对公钥进行数字签名,签名也需要使用公钥,公钥涉及到传输,那么又回到了中间人攻击的问题,我们就陷入了死循环。在 HTTPS 中,为了保证公钥的合法性,就需要一个证书,证书本质上是第三方结构(CA)对其进行签名后的公钥。浏览器已经把可信任的机构的公钥集成进来了,就不存在传输安全问题。
办法
因为我们没有证书这个环节,我们可以自己对传输的公钥进行签名,然后在构建时把验证签名的公钥的散列值记录在前端,当收到公钥时计算其散列值,如果散列值一致则公钥是合法的,就可以使用这个公钥进行传输解密。也就是说,我们需要两套 RSA 公钥 。
部署
既然涉及到公钥的传输,对于加密公钥,前端的获取主要有两种方式,一是调后端接口进行获取,这样的好处是后端可以定期生成新的证书,坏处是涉及到证书的合法性问题;另一种方法是运维在部署时,把证书的内容注入到前端的环境变量中,前端在构建时从环境中获取,打包到代码中。
对于签名公钥,我们只能才用第二种方式,第一种方式会让我们陷入死循环。 第二种方式存在的问题是,前端的代码是暴露的,既然无法避免中间人攻击,那么必然也可以通过逆向前端的代码,修改返回的数据。
在 HTTP 协议下不存在绝对的安全,所有的手段都是为了增加攻击者的成本,这也就是为什么我们需要 HTTPS 。