OpenSSL自签证书并基于Express搭建Web服务进行SSL/TLS协议分析起因最近在学习安全协议，大多数实验都是基于Windows下IIS，或者Linux下nginx搭建的Web服务，搭建环境和编写配置文件比较麻烦。而且我有多个不同环境的设备，折腾起来也比较麻烦，最好是开箱即用。所以我就用nodejs + express写了个跨平台的项目专门用于对比没有使用SSL/TLS的http与使用了的https协议。如果你对代码不感兴趣，仅是想要分析协议，可以到文章末尾的Github仓库中去下载该项目运行即可。搭建环境安装nodejs，访问官网https://nodejs.org，建议下载LTS版本并安装node --version npm --version运行上方命令出现版本号即可。在终端中运行如下命令安装yarnnpm -g install yarnyarn --version出现版本号即为成功。另外需要搭建OpenSSL环境，这个教程网上有很多，请自行搜索。代码部分接下来主要分享代码编写思路。目录结构创建项目目录为http_ssl，并在终端中进入mkdir http_ssl cd http_ssl使用yarn初始化，按照提示填写或者一路回车即可yarn init创建静态资源目录、配置文件目录、源码目录mkdir asset mkdir config mkdir src安装依赖添加express包等yarn add express在package.json文件中修改type为module，将语法为ES6，并编写启动命令scripts。（如果没有直接添加）"type":"module", "scripts":{ "start":"node ./src/index.js" }完整文件大致如下{ "name": "http_ssl", "version": "1.0.0", "main": "index.js", "author": "cairbin", "license": "MIT", "dependencies": { "express": "^4.19.2" }, "type":"module", "scripts":{ "start":"node ./src/index.js" } }初始文件我们希望http和https的端口号、主机地址之类的都能够放在配置文件里，而非代码中，这样方便我们更改，于是编写配置文件。touch config/config.js我们这里为了让配置文件能够像模块一样倒入，直接为js后缀，编辑该文件// config/config.js export default { server:{ static: '../asset', //静态资源目录，注意'../'，因为index.js在src下，否则找不到这个目录 http:{ host: "localhost", //主机地址 port: 8848 //端口号 }, https:{ host:'localhost', port:8849 } } }创建src/index.js为程序入口，我们编写的启动命令就是运行这个文件的touch src/index.js然后编写一个404.html的静态文件mkdir asset/html/ touch asset/html/404.html 404 Page 404 NOT FOUND 辅助模块项目还需要日志功能，我们没必要搞太复杂，想要功能强大的可以引入winston包，我这里就简单封装下js自带的console.*mkdir src/utils/ touch src/utils/logger.js// src/utils/logger.js const log = (options)=>{ console[options.type](`[${options.type.toUpperCase()}] ${options.msg}`) } const info = (msg)=>{ log({ type:"info", msg:msg }); } const warn = (msg)=>{ log({ type:'warn', msg:msg }); } const error = (msg)=>{ log({ type:'error', msg:msg }); } export default{ info,warn,error }控制器尽管是个Demo，但最好还是遵循MVC，我们创建目录和文件，编写一个简单控制器mkdir src/controller/ touch src/controller/defaultController.js// src/controller/defaultController.js import logger from './../utils/logger.js'; //日志模块 const defaultFunc = (req, res, next)=>{ logger.info('exec defultController.defaultFunc'); res.status(500).send('Hello World!'); //为了抓包分析方便，返回500状态码而非200 } export default{ defaultFunc }这里注意，为了抓包方便观察，我们返回的Code为500，而不是200。路由创建Express的路由mkdir src/route/ touch src/route/defaultRoute.js// src/route/defaultRoute.js import express from "express"; import defaultController from './../controller/defaultController.js'; import logger from './../utils/logger.js'; //日志模块 const router = express.Router(); router.get('/',(req,res,next)=>{ logger.info('defaultRoute, path=/'); defaultController.defaultFunc(req, res, next); }) export default router;创建服务创建http和https服务，指定静态资源目录，设置404页面，并集成使用以上编写的文件// src/index.js import express from 'express'; import https from 'https'; import http from 'http'; import defaultRoute from './route/defaultRoute.js' import fs, { copyFile } from 'fs'; import path from 'path'; import { dirname } from "node:path"; import { fileURLToPath } from "node:url" import config from './../config/config.js'; import logger from './utils/logger.js'; const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const app = express(); const httpsServer = https.createServer({ key:fs.readFileSync('keys/private.pem'), cert:fs.readFileSync('keys/file.crt') },app) const httpServer = http.createServer({},app); //static path app.use(express.static( path.join(__dirname, config.server.static))); logger.info(`static file path '${path.join(__dirname, config.server.static)}'`); app.use('/',defaultRoute); // 404 page app.use('*',(req,res)=>{ res.redirect('./html/404.html') }) httpsServer.listen(config.server.https.port, config.server.https.host, ()=>{ logger.info(`https service is running on https://${config.server.https.host}:${config.server.https.port}`); }) httpServer.listen(config.server.http.port, config.server.http.host, ()=>{ logger.info(`http service is running on http://${config.server.http.host}:${config.server.http.port}`); })你会发现keys这个用于存放证书的目录以及证书文件并不存在，我们接下来创建它们项目才能运行。另外需注意，上面的静态资源目录用path.join()来设为绝对路径。生成证书我们使用OpenSSL自签一个证书，在macOS或者Linux下可编写脚本touch generate.sh# generate.sh mkdir -p keys openssl genrsa 4096 > keys/private.pem openssl req -new -key keys/private.pem -out keys/csr.pem openssl x509 -req -days 365 -in keys/csr.pem -signkey keys/private.pem -out keys/file.crt然后运行脚本sh generate.sh如果是Windows的话也可以编写.bat批处理文件，或者直接执行命令：mkdir keys openssl genrsa 4096 > keys/private.pem openssl req -new -key keys/private.pem -out keys/csr.pem openssl x509 -req -days 365 -in keys/csr.pem -signkey keys/private.pem -out keys/file.crt注意旧版本的CMD或PowerShell可能不支持/，可以改用\，可能需要转义字符\\运行项目执行我们编写好的指令(package.json那里的)运行项目：yarn run start正常情况下，控制台应该是在报[INFO]浏览器地址栏输入http://localhost:8848，即可看到Hello World!页面（最指定协议头，有些浏览器会自动填充为https）https的话请访问https://localhost:8849，正常来讲现代的浏览器会阻止你访问这个页面，并提示非私人连接不安全，因为我们用的是自签名的证书。直接无视风险，继续访问(doge)抓包分析关闭页面，在浏览器的设置里，清除浏览器缓存（这一步很重要，因为我们之前访问过页面了，浏览器为了加速一般都会有缓存导致下次访问时不是向服务器请求，而是加载本地缓存的页面，导致抓包失败）启动wireshark并进行抓包，与其他情况不同，由于是回环地址，所以我们要抓loopback的包先来访问http未加密的页面，在wireshark中使用过滤器过滤http的包，该项目为了排除抓包的时候的干扰，访问页面返回的状态码为500，而不是200，所以我们只需要找到http status 为500的包即可查看数据，果然是明文再以https协议的方式访问一次并进行抓包发现抓不到了，我们将过滤器条件改为tls由此看到数据都被保护起来了，即使抓到也都是密文。Wireshark是直接看到TLS封包的内容的，我们可以配置环境变量，让浏览器得到的preMasterKey放到指定log中，wireshark支持读取这个文件，然后再进行抓包就可以查看加密的数据了。文章可以参考 https://segmentfault.com/a/1190000018746027代码仓库Click CairBin/SecurityProtocolLab