Nginx反向代理实现IoT设备HTTPS接入:从证书配置到MQTT over WebSocket实战
物联网设备上云,第一步要解决的不是功能,是安全。微信小程序要求HTTPS,苹果ATS策略要求TLS 1.2以上,工业网关数据明文传输被审计打回。所有这些问题的答案指向同一个方向:给IoT通信链路套上HTTPS外衣。但直接在EMQX或Mosquitto上配置SSL证书有坑:证书续期需要停机、WebSocket和TCP直连要分端口、前后端API和MQTT服务共用443端口会冲突。用Nginx做反向代理统一入口,是生产环境的标准做法。### 为什么IoT设备需要反向代理
先看不用Nginx的架构。EMQX默认开放四个端口:| 端口 | 协议 | 用途 | 安全性 ||------|------|------|--------|| 1883 | MQTT/TCP | 设备直连 | 明文 || 8883 | MQTT/TLS | 设备加密直连 | 需配证书 || 8083 | MQTT/WS | Web端连接 | 明文 || 8084 | MQTT/WSS | Web端加密连接 | 需配证书 |问题很明显:80和443端口被Web服务占了,MQTT只能用非标准端口。设备端配置wss://iot.example.com:8084/mqtt,端口暴露在外容易被扫描。证书配置在EMQX内部,Let's Encrypt续期时需要重启EMQX服务,导致设备断连。
加一层Nginx后,所有流量走443端口,Nginx根据路径分发:/api/转给后端应用,/mqtt转给EMQX的WebSocket端口,/转给前端静态文件。证书只配在Nginx层,续期不影响后端服务。### SSL证书申请与配置用Let's Encrypt申请免费证书。certbot支持standalone和webroot两种模式,IoT场景推荐webroot模式,不需要停Nginx:bash# 安装certbot sudo apt install certbot python3-certbot-nginx# 申请证书(Nginx必须已在运行,且80端口已配置)sudo certbot certonly --webroot -w /var/www/html -d iot.example.com# 证书文件位置# /etc/letsencrypt/live/iot.example.com/fullchain.pem# /etc/letsencrypt/live/iot.example.com/privkey.pem# 设置自动续期(certbot默认已添加cron) sudo systemctl status certbot.timer证书有效期90天,certbot会在到期前30天自动续期。webroot模式续期时Nginx不需要重启,只是重新加载证书文件。### Nginx完整配置下面是一份生产环境验证过的Nginx配置,覆盖HTTP重定向、HTTPS证书、WebSocket代理和API反向代理:nginx# /etc/nginx/conf.d/iot-platform.conf# HTTP重定向到HTTPSserver { listen 80; server_name iot.example.com; return 301 https://$server_name$request_uri;}# HTTPS主配置server { listen 443 ssl; http2 on; server_name iot.example.com; # SSL证书 ssl_certificate /etc/letsencrypt/live/iot.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/iot.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # HSTS头 add_header Strict-Transport-Security "max-age=31536000" always; # 前端静态文件 root /var/www/iot-dashboard; index index.html; location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_n proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 300s; proxy_send_timeout 300s; }}
几个关键配置项的解释:proxy_http_version 1.1必须开启。WebSocket握手基于HTTP/1.1,如果用默认的HTTP/1.0,Upgrade头会被丢弃,连接升级失败。proxy_set_header Connection "upgrade"强制设置Connection头为upgrade。不设置的话Nginx会在每次请求后关闭连接,WebSocket长连接保不住。proxy_read_timeout 300s把读取超时设为5分钟。默认60秒太短,MQTT设备的心跳间隔通常在30-120秒,如果心跳间隔超过proxy_read_timeout,Nginx会断开连接。建议设为心跳间隔的3倍以上。
四层代理:MQTT TCP直连方案WebSocket方案适合Web端和移动端,但纯硬件设备(STM32+ESP32)走原生MQTT TCP更省资源。Nginx的stream模块支持四层代理,可以转发TCP流量:```nginx# /etc/nginx/conf.d/iot-stream.confstream { # MQTT TLS直连代理 server { listen 8883 ssl; proxy_pass 127.0.0.1n }
}stream模块和http模块是平级的,不能嵌套在http块里。配置文件单独放在`conf.d`目录下,Nginx启动时自动加载。四层代理不解析应用层协议,只做TCP转发加SSL终止。性能比七层代理高约30%,但没法根据URL路径做路由。实际项目中两种代理配合用:设备TCP直连走8883端口(四层),Web端WSS走443端口(七层)。### 设备端连接代码ESP32用Arduino框架连接MQTT over WSS的代码:cpp#include <WiFiClientSecure.h>#include <PubSubClient.h>
#include <WiFi.h>WiFiClientSecure esp_client;PubSubClient mqtt_client(esp_client);const char* ssid = "YourWiFi";const char* password = "WiFiPassword";const char* mqtt_server = "iot.example.com";const int mqtt_port = 443;void setup() { Serial.begin(115200); WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } // 跳过证书验证(生产环境应加载根证书) esp_client.setInsecure(); mqtt_client.setServer(mqtt_server, mqtt_port); mqtt_client.setCallback(mqtt_callback);
}void loop() { if (!mqtt_client.connected()) { while (!mqtt_client.connect("esp32_001")) { delay(2000); } mqtt_client.subscribe("device/esp32_001/cmd"); } mqtt_client.loop();}void mqtt_callback(char* topic, byte* payload, unsigned int length) {
// 处理下行指令}````setInsecure()跳过证书验证,仅适合测试环境。生产环境应该用setCACert()加载Let's Encrypt的根证书。根证书可以从浏览器导出,也可以在代码中硬编码PEM格式字符串。### 连接稳定性优化IoT设备的网络环境不稳定,断线重连是常态。Nginx层的优化主要集中在超时参数和连接复用上:| 参数 | 默认值 | 推荐值 | 原因 ||------|--------|--------|------|| proxy_read_timeout | 60s | 300s | 适配MQTT心跳间隔 | | proxy_send_timeout | 60s | 300s | 避免下行指令超时 || keepalive_timeout | 65s | 120s | 减少TLS握手开销 || ssl_session_cache | none | 10m | 复用TLS会话 || worker_connections | 512 | 4096 | 支撑更多并发设备 |ssl_session_cache设为shared:SSL:10m`后,多个worker进程共享TLS会话缓存,设备重连时可以跳过完整的TLS握手,连接建立时间从200ms降到50ms。对1000台设备同时重连的场景,这个优化能把重连风暴的持续时间从40秒压缩到10秒。
开发调试工具部署Nginx反向代理后,调试IoT设备连接需要抓包确认MBAP报头和MQTT握手过程。我们团队在heicat.com开源了多款开发工具,ESP32工具箱的串口监视器模块可以实时查看设备端MQTT连接日志。PHP工具箱中的网络调试模块也能辅助验证API反向代理的响应。更多开源项目在GitHub:https://github.com/huwangkeji### 小结Nginx反向代理给IoT通信链路带来的核心价值是统一入口和SSL卸载。证书管理集中在一处,后端服务不需要处理TLS,设备端也不需要各自配置证书。四层stream代理覆盖原生MQTT TCP直连,七层http代理覆盖WebSocket和API转发,两种模式配合使用,一套Nginx实例就能支撑整个IoT平台的网络接入层。
实际部署中最容易踩的坑是proxy_read_timeout和心跳间隔的匹配。Nginx默认60秒超时,如果设备心跳间隔设为120秒,连接每隔60秒就被Nginx断一次。把超时调到心跳间隔的3倍以上,再配合ssl_session_cache做TLS会话复用,设备重连的体验就能做到用户无感。