Node.js 部署免費/自動續訂 HTTPS

linkFly發表於2019-02-16

隨著網際網路快速發展,網際網路資訊保安越來越受到大家重視,HTTPS 應該是近兩年各大廠商都在盡力普及的技術之一。國內大廠基本上已經全面普及了 HTTPS。

本文首發於我的個人網站:聽說 - tasaid.com/,建議在我的個人網站閱讀,擁有更好的閱讀體驗。

前端開發 QQ 群:377786580

HTTPS 現狀

早在 2016 年底,我就寫過 《 從 HTTP 到 HTTPS 系列 》文章來講解 HTTPS。當時結合本站的部署經驗,給大家詳細介紹了 《 IIS 部署免費 HTTPS 》。

這篇文章就跟大家介紹一下 Node.js 如何部署免費 HTTPS 以及簡單的部署 HTTP/2

截止 2018 年 03 月 13 日,由 Let's Encrypt 實時統計報告 顯示,在統計的 6930 多萬活躍網站中,已經有 5350 萬(約 77%)的站點部署了 HTTPS 證書服務。

lets encrypt stats

同時 Google 透明度報告 - 網路上的 HTTPS 加密 中,統計了使用 Chrome 瀏覽器,訪問的站點統計中,HTTPS 使用率的增長情況:

google stats

而在今年 2 月份,Chrome 團隊也宣佈,將在 2018 年 7 月份釋出的 Chrome 68 中,將沒有部署 HTTPS 的網站標記為 "不安全"。

chrome 68 status

簡而言之,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 證書的部署,只需要引入這個包並使用,就可以:

  1. 自動註冊 Let's Encrypt 證書
  2. 自動續訂( 80 天左右),且伺服器無需重啟
  3. 支援虛擬主機

並且 greenlock 相關的證書生態圈十分完善,同樣有支援 koagreenlock-koa

安裝和使用

通過 npm 安裝 greenlock-express

$ npm install --save greenlock-express@2.x
複製程式碼

使用起來非常簡單,這是 greenlock-express 預設提供的 demo:

const greenlock = require('greenlock-express')

require('greenlock-express').create({
  // 測試
  server: 'staging',
  // 聯絡郵箱
  email: 'john.doe@example.com',
  // 是否同意 Let's Encrypt 條款... 這必須為 true 啊,不然走不下去
  agreeTos: true,
  // 申請的域名列表,不支援萬用字元
  approveDomains: [ 'tasaid.com', 'www.tasaid.com' ],
  // 繫結 express app
  app: require('express')().use('/', function (req, res) {
    res.end('Hello, World!');
  })
}).listen(80, 443)
複製程式碼

證書存在 ~/letsencrypt

當然上面程式碼只能用於測試/開發環境,因為它並沒有申請一個有效的證書,而是生成了一個自簽名的證書(跟以前的 12306 自簽證書一樣),用於在開發環境中除錯。

API

greenlock-expresscreate(options) 函式引數簽名如下:

interface Options {
  /**
   * Express app
   */
  app: Express
  /*
   * 遠端伺服器
   * 測試環境中可用為 staging
   * 生產環境中為 https://acme-v01.api.letsencrypt.org/directory
   */
  server: string
  /**
   * 用於接收 let's encrypt 協議的郵箱
   */
  email: string
  /**
   * 是否同意協議
   */
  agreeTos: boolean
  /**
   * 在註冊域名獲取證書前,會執行這個回撥函式
   * string[]: 一組需要註冊證書的域名
   * 函式: 第一個引數跟 Options 格式差不多,第二個引數是當前自動獲取的域名資訊,第三個引數是在處理完之後傳遞的回撥函式
   */
  approveDomains: string[] | (opts, certs: cb) => any
  /**
   * 更新證書最大天數 (以毫秒為單位)
   */
  renewWithin: number
  /**
   * 更新證書的最小天數(以毫秒為單位)
   */
  renewBy: number
}
複製程式碼

經過測試,在真實的生產環境中, approveDomains 必須為函式,傳陣列的話不會生效。

生產環境

生產環境中部署還需要做一些配置改動和引入一些包。

更新包:

$ npm i --save greenlock-express@2.x
$ npm i --save le-challenge-fs
$ npm i --save le-store-certbot
$ npm i --save redirect-https
複製程式碼

生產程式碼:

const greenlock = require('greenlock-express')
const express = require('express')

const app = express()


const lex = greenlock.create({
  // 注意這裡要成這個固定地址
  server: 'https://acme-v01.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 = certs.altnames
      opts.domains = [ 'tasaid.com', 'www.tasaid.com' ]
    } else {
      opts.email = '你的郵箱@live.com'
      opts.agreeTos = true
    }
    cb(null, { options: opts, certs: certs })
  },
})

// 這裡的 redirect-https 用於自動將 HTTP 請求跳到 HTTPS 上
require('http').createServer(
  lex.middleware(
    require('redirect-https')()
   )
  ).listen(80, function () {
    console.log('Listening', `for ACME http-01 challenges on: ${JSON.stringify(this.address())}`)
})
// 繫結 HTTPS 埠
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/2HTTP/1.1 的升級版,主要來說改進了這些地方:

  1. 二進位制協議:採用二進位制流
  2. 多路複用:一次請求多次複用管道
  3. 伺服器推送:解決 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/2Node.js 中也十分簡單,只需要引入 spdy 包即可:

$ npm i --save spdy
複製程式碼

然後我們把上一節的程式碼做一點修改即可支援 HTTP/2:

const greenlock = require('greenlock-express')
const express = require('express')
// HTTP/2
const spdy = require('spdy')

const app = express()


const lex = greenlock.create({
  // 注意這裡要成這個固定地址
  server: 'https://acme-v01.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 = certs.altnames
      opts.domains = [ 'tasaid.com', 'www.tasaid.com' ]
    } else {
      opts.email = '你的郵箱@live.com'
      opts.agreeTos = true
    }
    cb(null, { options: opts, certs: certs })
  },
})

// 這裡的 redirect-https 用於自動將 HTTP 請求跳到 HTTPS 上
require('http').createServer(
  lex.middleware(
    require('redirect-https')()
   )
  ).listen(80, function () {
    console.log('Listening', `for ACME http-01 challenges on: ${JSON.stringify(this.address())}`)
})

// HTTP/2
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 相關的技術應用,會在後續篇幅中再為大家講解。

本文首發於我的個人網站:聽說 - tasaid.com/,建議在我的個人網站閱讀,擁有更好的閱讀體驗。

前端開發 QQ 群:377786580

相關文章