随着互联网快速发展,互联网信息安全越来越受到大家重视,HTTPS
应该是近两年各大厂商都在尽力普及的技术之一。国内大厂基本上已经全面普及了 HTTPS。
前端开发 QQ 群:377786580
HTTPS 现状
早在 2016 年底,我就写过 《 从 HTTP 到 HTTPS 系列 》文章来讲解 HTTPS
。当时结合本站的部署经验,给大家详细介绍了 《 IIS 部署免费 HTTPS 》。
这篇文章就跟大家介绍一下 Node.js
如何部署免费 HTTPS
以及简单的部署 HTTP/2
。
截止 2018 年 03 月 13 日,由 Let’s Encrypt 实时统计报告 显示,在统计的 6930 多万活跃网站中,已经有 5350 万(约 77%)的站点部署了 HTTPS
证书服务。
同时 Google 透明度报告 - 网络上的 HTTPS 加密 中,统计了使用 Chrome 浏览器,访问的站点统计中,HTTPS 使用率的增长情况:
而在今年 2 月份,Chrome 团队也宣布,将在 2018 年 7 月份发布的 Chrome 68 中,将没有部署 HTTPS 的网站标记为 “不安全”。
简而言之,HTTPS 大势所趋。
Node.js 部署 HTTPS
早在 《 从 HTTP 到 HTTPS - IIS 部署免费 HTTPS 》一文中,我就指出了 Let’s Encrypt 免费证书的优势:
由 ISRG(Internet Security Research Group,互联网安全研究小组)提供服务,免费、访问速度快,稳定等。
所以这次部署的证书也是围绕 Let's Encrypt
展开。
greenlock-express
由于 js 生态圈的繁华,所以想找一个现有的包是件很轻松的事情,greenlock-express 这个包就帮助我们封装了 Let's Enctrypt
证书的部署,只需要引入这个包并使用,就可以:
- 自动注册
Let's Encrypt
证书
- 自动续订( 80 天左右),且服务器无需重启
- 支持虚拟主机
并且 greenlock
相关的证书生态圈十分完善,同样有支持 koa
的 greenlock-koa。
注意:greenlock-express
在 2.1.1
之后开始支持由 Let’s Encrypt 2018 年开始发布的 ACME v2 协议证书。
从 2.1.1 之前升级上来的 greenlock-express
会有 API 变动,如果直接升级会报证书错误:
Let’s Encrypt v1 is deprecated.
Please update to Let’s Encrypt v2 (ACME draft 12)
如果想要继续使用 v1的证书服务,请先安装包:
1
| npm install --save le-acme-core
|
然后修改以下代码:
1 2 3 4 5 6 7
| require('greenlock-express').create({ version: 'v01', server: 'https://acme-v01.api.letsencrypt.org/directory', })
|
使用 ACME draft 12 证书有个非常好的用处,就是 支持通配符证书 了。
安装和使用
通过 npm
安装 greenlock-express
:
1
| $ npm install --save greenlock-express@2.x
|
使用起来非常简单,这是 greenlock-express
默认提供的 demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const greenlock = require('greenlock-express')
require('greenlock-express').create({ server: 'https://acme-staging-v02.api.letsencrypt.org/directory', email: 'john.doe@example.com', agreeTos: true, approveDomains: [ 'tasaid.com', 'www.tasaid.com' ], app: require('express')().use('/', function (req, res) { res.end('Hello, World!'); }) }).listen(80, 443)
|
证书存在 ~/letsencrypt
。
当然上面代码只能用于测试/开发环境,因为它并没有申请一个有效的证书,而是生成了一个自签名的证书(跟以前的 12306 自签证书一样),用于在开发环境中调试。
API
greenlock-express
的 create(options)
函数参数签名如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| interface Options { version: 'draft-11' | 'v01'
app: Express
server: string
email: string
agreeTos: boolean
approveDomains: string[] | (opts, certs: cb) => any
renewWithin: number
renewBy: number }
|
经过测试,在真实的生产环境中, approveDomains
必须为函数,传数组的话不会生效。
生产环境
生产环境中部署还需要做一些配置改动和引入一些包。
更新包:
1 2 3 4 5
| $ npm i --save greenlock-express@2.x $ npm i --save ursa $ npm i --save le-challenge-fs $ npm i --save le-store-certbot $ npm i --save redirect-https
|
生产代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| const greenlock = require('greenlock-express') const express = require('express')
const app = express()
const lex = greenlock.create({ version: 'draft-11', server: 'https://acme-v02.api.letsencrypt.org/directory', challenges: { 'http-01': require('le-challenge-fs').create({ webrootPath: '~/letsencrypt/var/acme-challenges' }) }, store: require('le-store-certbot').create({ webrootPath: '~/letsencrypt/srv/www/:hostname/.well-known/acme-challenge' }), approveDomains: (opts: any, certs: any, cb: any) => { appLog.info('approveDomains', { opts, certs }) if (certs) {
opts.domains = [ 'tasaid.com', 'www.tasaid.com' ] } else { opts.email = '你的邮箱@live.com' opts.agreeTos = true } cb(null, { options: opts, certs: certs }) }, })
require('http').createServer( lex.middleware( require('redirect-https')() ) ).listen(80, function () { console.log('Listening', `for ACME http-01 challenges on: ${JSON.stringify(this.address())}`) })
require('https').createServer( lex.httpsOptions, lex.middleware(app) ).listen(443, function () { console.log(('App is running at http://localhost:%d in %s mode'), app.get('port'), app.get('env')) console.log('Press CTRL-C to stop\n') })
|
如果没有生效,可以检查下 ~/letsencrypt
的证书信息,和 443 端口是否打开。
部署 HTTP/2
HTTP/2 是 HTTP/1.1 的升级版,主要来说改进了这些地方:
- 二进制协议:采用二进制流
- 多路复用:一次请求多次复用管道
- 服务器推送:解决 HTTP/1.x 时代最大的痛点
值的注意的是,HTTP/2 是支持 HTTP 协议的,只不过浏览器厂商都不愿意支持 HTTP,所以基本上可以认为,用上 HTTP/2 的前置条件是必须部署 HTTPS。
SPDY
早在 2009 年,Google 开发了一个实验性协议,叫做 SPDY,目的解决 HTTP/1.x 中的一些设计缺陷。在 SPDY 发布几年后,这个新的实验性协议得到了 Chrome、Firefox 和 Opera 的支持,应用越来越广泛。然后 HTTP 工作组 (HTTP-WG) 在这个 SPDY 的基础上,设计了 HTTP/2,所以可以说 SPDY 是 HTTP/2 的前身。
关于 HTTP/2 的详情可以参考 这篇文章。
部署 HTTP/2
引入 HTTP/2
在 Node.js
中也十分简单,只需要引入 spdy
包即可:
然后我们把上一节的代码做一点修改即可支持 HTTP/2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| const greenlock = require('greenlock-express') const express = require('express')
const spdy = require('spdy')
const app = express()
const lex = greenlock.create({ version: 'draft-11', server: 'https://acme-v02.api.letsencrypt.org/directory', challenges: { 'http-01': require('le-challenge-fs').create({ webrootPath: '~/letsencrypt/var/acme-challenges' }) }, store: require('le-store-certbot').create({ webrootPath: '~/letsencrypt/srv/www/:hostname/.well-known/acme-challenge' }), approveDomains: (opts: any, certs: any, cb: any) => { appLog.info('approveDomains', { opts, certs }) if (certs) {
opts.domains = [ 'tasaid.com', 'www.tasaid.com' ] } else { opts.email = '你的邮箱@live.com' opts.agreeTos = true } cb(null, { options: opts, certs: certs }) }, })
require('http').createServer( lex.middleware( require('redirect-https')() ) ).listen(80, function () { console.log('Listening', `for ACME http-01 challenges on: ${JSON.stringify(this.address())}`) })
spdy.createServer(lex.httpsOptions, lex.middleware(app)).listen(443, function () { console.log('Listening https', `for ACME tls-sni-01 challenges and serve app on: ${JSON.stringify(this.address())}`) })
|
至于 HTTP/2 相关的技术应用,会在后续篇幅中再为大家讲解。
2018-08-23,今天无意中发现 log4js 没有写请求入口的 access 日志,经排查发现是因为开启了 HTTP/2 导致的
通过 spdy
启动了 HTTP/2 的服务之后,log4js 使用 connectLogger
注入 express
中间件的时候,在入口处不会写 access 日志:
1 2
| app.use(log.Log4js.connectLogger(log.accessLogger, { level: log.accessLogger.level }))
|
具体的现象比较怪异:在浏览器中访问的时候没有 access 日志,但是搜索引擎抓取的时候却有 access 日志,于是怀疑是不是 HTTP/2 导致的问题。
查到 log4js/lib/connect-logger.js 源码之后,发现 log4js
在注入 express
中间件的时候,监听了 express
的 finish
事件,搜索引擎抓取的时候会主动关闭 HTTP/2 的长连接,所以自然就执行了 finish
事件。
而开启了 HTTP/2 之后在浏览器中打开页面是不会触发 finish
事件的,从而导致 log4js
没有写 access 日志。
最终我选择的解决方案是下掉 HTTP/2。因为 HTTP/2 的 server push 和多路复用,在现代 web 中大部分只适用于 CDN 场景,said 项目本身用不到这些特性。
前端开发 QQ 群:377786580