接、
普通的 TCP 通信数据是明文传输的,所以存在数据泄露和被篡改的风险,我们可以写一段测试代码试验一下。
TCP Server:
- const net = require('net');
- const server = net.createServer();
- const serverHost = '127.0.0.1';
- const serverPort = 8888;
- server.on('connection', (clientSocket) = >{
- clientSocket.setEncoding('utf8');
- clientSocket.on('data', (data) = >{
- console.log(`client say: $ {
- data
- }`);
- });
- clientSocket.on('error', () = >{});
- });
- server.listen({
- host: serverHost,
- port: serverPort
- },
- () = >{
- console.log(`server is listening on port $ {
- 8888
- }`)
- });
TCP Client:
- const net = require('net');
- const socket = new net.Socket();
- const serverHost = '127.0.0.1';
- const serverPort = 8888;
- let index = 0;
- socket.on('error', () = >{});
- socket.connect({
- host: serverHost,
- port: serverPort
- },
- () = >{
- console.log(`client has connected to host $ {
- serverHost
- },
- port $ {
- serverPort
- }`);
- setInterval(() = >{
- socket.write(`i love u $ {
- index++
- }`);
- },
- 3000);
- });
启动 Server 和 Client 后,可以在 Server 的控制台中看到来自 Client 的消息:
- client say:i love u 0
- client say:i love u 1
- client say:i love u 2
- client say:i love u 3
- client say:i love u 4
数据在传输的过程中是可以被所有人看到的,可以用 WireShark 抓包测试一下。由于 WireShark 无法直接抓取发送给本地的 TCP 包,我将 Server 部署到了另外一台机器上,需要做如下修改:
配置好抓取 IP:
抓包:
可以看到,表白信息全被别人看了去了 :(
可能有人会说:我脸皮厚,随便看~
但是要注意了,所有 http 协议的请求,他们的数据都是这样发送的!可以认为,在一个使用 http 协议而不是 https 协议的网站上,你的游戏账号、银行卡密码,都是这样赤果果的暴露在别人眼前的!
不仅如此,别人还可以随意篡改你的数据!
我们上网的过程中,数据从我们的电脑到达目标服务器的过程中,可能会经过层层代理和多次路由,最终才到达目标服务器并不是像上面我们的 Demo 那样是直连的!
为了模拟这种情况,我们可以在 Demo 的 Client 和 Server 之间加上一个耿直的 Proxy:
- const net = require('net');
- const proxyServer = net.createServer();
- const proxyHost = '127.0.0.1';
- const proxyPort = 8889;
- const serverHost = '127.0.0.1';
- const serverPort = 8888;
- //代理连接到真实目标Server
- const proxySocket = new net.Socket();
- proxySocket.connect({
- host: serverHost,
- port: serverPort
- },
- () = >{
- console.log(`proxy has connected to host $ {
- serverHost
- },
- port $ {
- serverPort
- }`);
- });
- //启动代理Server
- proxyServer.on('connection', (clientSocket) = >{
- //直接将客户端的数据发给真实目标Server
- clientSocket.pipe(proxySocket);
- });
- proxyServer.listen({
- host: proxyHost,
- port: proxyPort
- },
- () = >{
- console.log(`proxy server is listening on port $ {
- 8889
- }`)
- });
修改 Client 的连接端口,连到 proxy 的 8889 端口而不是真实目标的 8888 端口,依次启动 Server→Proxy→Client,可以看到 Server 收到了:
- client say:i love u 0
- client say:i love u 1
- client say:i love u 2
- client say:i love u 3
需要注意到这一行代码
, 所以说这是一个耿直的代理:)
- clientSocket.pipe(proxySocket)
换一个不耿直的代理,它会这样做:
- clientSocket.setEncoding('utf8');
- clientSocket.on('data',(data)=>{
- data=data.replace(/love/g,'hate');
- proxySocket.write(data);
- });
重新依次启动 Server→Proxy→Client,可以看到 Server 收到了:
- client say:i hate u 0
- client say:i hate u 1
- client say:i hate u 2
- client say:i hate u 3
- client say:i hate u 4
这下 shabi 了吧,妹子肯定是追不到了:( 咋办呢?
先梳理一下思路:按照之前了解的加密算法原理,我们可以让 Server 给 Client 下发一份非对称加密的公钥,client 用公钥加密数据然后发送,这样就不存在数据泄露和篡改的风险了。
然而,这个世界是很险恶的,会有人把自己伪装成 Server,给 Client 下发他们自己的公钥,并拦截真实 Server 下发给 Client 的真实公钥。
由于我们没办法判定 Client 拿到的公钥是真实 Server 还是恶意代理发过来的,所以我们需要一个可信赖的第三方,来告诉 Client 拿到的公钥到底是不是可信的,这个第三方就是 CA 机构,Certificate Authority,证书授权中心。
引入了 CA 机构后,获取证书流程如下:
在实际 Client 应用中,例如浏览器中,扮演可信角色——CA 机构的实际上是浏览器提前内置好的,一部分浏览器厂商认为可信的 CA 机构的根证书,下面演示一下如何创办一家 CA 机构,并为一个服务器颁发 CA 证书。
我们可以利用开源的 openssl 库来创办一家私人的 CA 机构,上面的演示 Demo 目录结构为:
- ├─client
- │ client.js
- ├─proxy
- │ proxy.js
- └─server
- server.js
新建一个 CA 目录,创建一家 CA 机构,可以通俗地理解为:
这样,我们就得到了一份称为 "根证书" 的证书文件,浏览器如果信任我们的 CA 机构,就可以把我们的根证书内置到浏览器中。
对应的 openssl 命令为:
创建一个 2048 位的非对称加密私钥
- openssl genrsa -out caPrivate.key 2048
通过私钥创建一个正式签名请求文件,期间会要求输入机构名称、地址、email 等信息。
- openssl req -new -key caPrivate.key -out ca.csr
使用 x509 证书协议为刚刚创建的证书签名请求签名,得到 ca.crt 文件,即 "根证书"。
- openssl x509 -req -in ca.csr -signkey caPrivate.key -out ca.crt
至此,我们成功创办了一家拥有自己根证书的 CA 机构,文件列表:
ca.crt
ca.csr
caPrivate.key
证书的细节远不止这么简单,具体的可以参见 CA 证书标准 X.509,
为了安全,我们升级一下前边 Demo 中的 Server,创建自己的证书,并请求 CA 机构签名颁发 CA 证书,来进行 TLS 安全通信。
- openssl genrsa -out private.key 2048
- openssl req -new -key private.key -out request.csr
CA 机构为 LtsServer 的证书签名,并颁发 CA 证书文件 server.crt
- openssl x509 -req -CA ../CA/ca.crt -CAkey ../CA/caPrivate.key -CAcreateserial -in request.csr -out server.crt
这里要注意的是,本地测试的时候,
属性要填写 localhost,若填写线上应用地址,则使用时客户端会报错:
- Common Name
- Error: Hostname/IP doesn't match certificate's altnames: "Host: localhost. is not cert's CN: zoucz.com"
使用上面一步颁发的 CA 证书来进行 TLS 通信,需要三个步骤:
① TLS Server:
- const tls = require('tls');
- const fs = require('fs');
- const serverHost = '127.0.0.1';
- const serverPort = 8888;
- const options = {
- key: fs.readFileSync('private.key'),
- cert: fs.readFileSync('server.crt'),
- };
- var tlsServer = tls.createServer(options, (clientSocket) = >{
- clientSocket.setEncoding('utf8');
- clientSocket.on('data', (data) = >{
- console.log(`client say: $ {
- data
- }`);
- });
- clientSocket.on('error', (e) = >{
- console.log(e)
- });
- });
- tlsServer.listen({
- host: serverHost,
- port: serverPort
- },
- () = >{
- console.log(`lts server is listening on port $ {
- 8888
- }`)
- });
② 将 CA 机构根证书内置到 Client 中:
③ 创建 TLS Client
- const tls = require('tls');
- const fs = require('fs');
- const serverHost = '127.0.0.1';
- const serverPort = 8888;
- const options = {
- ca: [fs.readFileSync('ca.crt')]
- };
- let index = 0;
- var tlsSocket = tls.connect(serverPort, options, () = >{
- console.log(`tls client has connected to host $ {
- serverHost
- },
- port $ {
- serverPort
- }`);
- setInterval(() = >{
- tlsSocket.write(`i love u $ {
- index++
- }`);
- },
- 3000);
- });
- tlsSocket.on('error', (e) = >{
- console.log(e)
- });
再将服务端部署到另外一台机器上,抓包:
现在看到的内容就是乱码了,没有内容泄露的风险。同理,在数据传输的过程中,第三方也无法篡改我们的数据了。
将自己的测试 TLS 服务部署到另外一台机器上时,有个要注意的地方,TlsClient 的 option 中需要修改如下:
- const options = {
- ca: [ fs.readFileSync('ca.crt') ],
- checkServerIdentity: function (host, cert) {
- return undefined;
- }
- };
这是因为 TLS 通信时,对于服务端身份的检查,使用域名和使用 IP 的情况下,验证的策略不同,当我们在本地测试,使用 IP 时,需要将 IP 加入证书的 SAN 扩展 (Subject Alternative Name) 中,关于此扩展的内容, 可以到查询,我没有深入研究。
前边 1.1 小节中说道,http 协议是基于 tcp 传输协议的不安全协议,那么 https 协议为什么被认为是安全的协议呢? 答案就是,它是基于 tls 传输协议的应用层协议。
有了前边对 LTS 通信原理的了解,再来看 https 就非常简单了,我们可以直接复用刚刚为 TLS Server 颁发的 CA 证书,来创建一个 https 服务器。
- var https = require('https');
- var fs = require('fs');
- var options = {
- key: fs.readFileSync('./private.key'),
- cert: fs.readFileSync('./server.crt')
- };
- https.createServer(options,
- function(req, res) {
- res.writeHead(200);
- res.end('hello https');
- }).listen(8866);
chrome 会这样提示你,我们的浏览器里边找不到为这个服务器 CA 证书签名的 CA 证书,这很可能是一个骗子网站,这是因为我们的 CA 机构根证书没有被内置到 chrome 里边。点继续访问:
查看证书:
可以将我们的 CA 机构根证书导入 chrome,在 chrome 设置中:
重启 chrome,再次访问我们的 https 服务
看,变成小绿锁了~
最后,本文所有 Demo 代码存放于:
来源: