[Node.js] 用户自定义域名 https 访问绑定证书的方法

作者 huhamhire,暂无评论,2016年7月9日 17:46 程序实践

问题背景

最近在做线上产品的时候遇到了一个比较麻烦的问题,用户需要绑定自己的域名。但是由于线上服务器已经处于全站 https 的状态,需要用户提供自己域名对应的证书和私钥,证书信息可能会考虑存在后端的数据库以及缓存中。为了更加灵活的提供自定义域名的绑定服务,就需要后端服务来实现支持 SNI (Server Name Indication) 的加密处理。

其实在平时使用 CDN 服务的时候,服务商大多都支持用户绑定自定义域名并上传证书信息。以前一直挺好奇这方面的实现原理,但是这方面现成的资料并不多,绝大多数的项目应该都遇不到这个需求,感觉可以自己来折腾一下。

显然如果用 nginx 来做这种场景下的加密层的话,会有每次更新证书需要 reload 服务的缺陷,如果用户自定义域名的情况比较多,会非常不易维护。除非自己去 hack 相关的模块,显然这样的维护代价还是有点高的。所以还是需要考虑其他方式来实现。

node.js 实现 SNI 加密访问的原理

因为项目上的后端是 node.js 的,所以很自然的优先研究起了 node.js 的实现方法。看下 node.js 自带的 https 模块,可以发现 https.Server 本身就是一个 tls.Server 的一个子类。TLS 层本身只是实现了加密的处理,https 模块则对连接增加了访问协议的支持。

https 模块本身没有太多加密相关的接口说明,tls 模块则提供了比较多的说明,https://nodejs.org/api/tls.html。注意到 tls.createServer 中存在一个 SNICallback 参数,其说明如下:

SNICallback(servername, cb) <Function> A function that will be called if the client supports SNI TLS extension. Two arguments will be passed when called: servername and cb. SNICallback should invoke cb(null, ctx), where ctx is a SecureContext instance. (tls.createSecureContext(...) can be used to get a proper SecureContext.) If SNICallback wasn't provided the default callback with high-level API will be used.

大致就是客户端在请求建立连接的时候,会向这个方法传入域名信息,以供建立加密的连接 context。如果这个方法不返回 context ,则自动使用默认的连接设置。所以,通过这个接口来生成用户所需域名的加密连接应该是可行的。

另外需要说明一下,早期的 SSLv2 默认一台服务器只提供一项服务,没有对 SNI 提供支持。到了后来的 TLS 规范为了迎合实际情况,增加了 SSL 握手请求时,想服务端提供主机名的方案来实现 SNI。不过由于历史原因,早期的浏览器如 IE 6 就不支持 SNI。不过因为现在的的线上服务一般都可以不考虑如此老旧的浏览器了,所以 SNI 的功能直接用就行。

node.js 实现 SNI 加密访问

接下来可以写一个简单的 demo 来测试一下实际的访问效果。需要预先准备好至少两个 https 证书,比如我这里准备了 hamhire.comhuhamhire.com 的证书和私钥。这里测试代码使用的是 node 4.4.4 版本,额外还用到了 express 框架。测试代码如下:


'use strict';
const https = require('https');
const tls = require('tls');
const fs = require('fs');

const app = require('express')();

app.use('/', (req, res) => {
    res.send('Hello, World!').end();
});

https.createServer({
    key: fs.readFileSync('certs/hamhire.com.key'),
    cert: fs.readFileSync('certs/hamhire.com.crt'),
    secureProtocol: 'SSLv23_method',
    honorCipherOrder: true,
    SNICallback: (servername, callback) => {
        console.log(`visit: ${ servername }`);
        if (servername === 'test.huhamhire.com') {
            let ctx = tls.createSecureContext({
                key: fs.readFileSync('certs/huhamhire.com.key'),
                cert: fs.readFileSync('certs/huhamhire.com.crt')
            });
            return callback(null, ctx);
        }
        return callback();
    }
}, app).listen(443, () => {
    console.log('SSL Proxy listening on port 443');
});

服务启动之后,本地可以修改一下 hosts 文件,把指定的域名指向到本机上。然后分别测试一下 test.hamhire.comtest.huhamhire.com 两个域名的访问。

可以看到访问 test.hamhire.com 时,服务器使用了 *.hamhire.com 的证书。

hamhire_com_test

而在访问 test.huhamhire.com 时,服务器则使用了 *.huhamhire.com 的证书。

huhamhire_com_test

另外,在控制台也可以得到对应域名访问的信息。

console

我找到的关于 node.js 提供 SNI 访问的大致实现思路就是这样,实际部署的时候,node 服务器外层应该还需要有转发 TCP 数据的负载均器。

关键词:node.js , SNI , TLS , 自定义域名
登录后进行评论