ESP32 实战教程:HTTPS 与 TLS 安全连接的正确打开方式

让 ESP32 访问一个 HTTP 接口很容易,换成 HTTPS 后,证书、系统时间、握手超时和内存占用就会一起找上门。有人遇到证书错误,第一反应是 setInsecure();它确实能让请求跑起来,却相当于把“请验证服务器身份”这件事删掉了。本文从正确校时和 CA 校验开始,做一个可以上线维护的 HTTPS 客户端。

ESP32 HTTPS/TLS 安全连接

一、TLS 解决了什么

HTTPS 在 HTTP 外面加了一层 TLS,主要解决三件事:加密传输、防止内容被中途修改、验证服务器确实是你要访问的那个。ESP32 连接服务器时,会检查服务器证书是否由可信 CA 签发、域名是否匹配、证书是否在有效期内。

如果设备没有正确时间,证书的“尚未生效”和“已经过期”判断都会失败。于是 NTP 校时不是附加功能,而是 TLS 初始化的前置步骤。设备刚上电时网络可能还没稳定,校时也可能失败,所以要有重试和超时,而不是在 setup() 里无限等待。

二、硬件和内存预期

TLS 握手会消耗比普通 TCP 更多的 RAM。ESP32 老型号在 Wi-Fi、摄像头、音频和 TLS 同时工作时,容易遇到堆内存不足。正式项目应打印 ESP.getFreeHeap(),减少一次性大字符串,尽量使用流式读取响应。

服务器端必须拥有有效证书。开发阶段可以使用公开 HTTPS API 或自己的测试服务器;不要把生产服务器证书私钥放进固件,设备只需要内置信任的 CA 根证书或中间证书。

三、先连接 Wi-Fi,再校准时间

HTTPS 请求的正确顺序

#include <WiFi.h>
#include <time.h>

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

bool waitForTime(uint32_t timeoutMs) {
  configTime(8 * 3600, 0, "ntp.aliyun.com", "pool.ntp.org");
  uint32_t start = millis();
  time_t now = 0;
  while (millis() - start < timeoutMs) {
    time(&now);
    if (now > 1700000000) return true;
    delay(200);
  }
  return false;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
    delay(250);
  }
}

不要只判断 WiFi.status() == WL_CONNECTED 就马上握手。还要确认 DNS 和默认网关可用;如果设备长期离线,时间可以从 RTC 或上次成功时间恢复,但在证书校验完成前不要把这个估算时间当成绝对可靠。

四、加载 CA 根证书

WiFiClientSecure 支持通过 setCACert() 设置可信 CA。证书以 PEM 字符串放在固件中会占用一部分 Flash,但安全性远高于 setInsecure()。证书轮换时需要重新发布固件,或者把证书链存储在 LittleFS,并设计签名校验和回退证书。

#include <WiFiClientSecure.h>

static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
YOUR_ROOT_CA_CERTIFICATE
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure client;

bool prepareTls() {
  client.setCACert(ROOT_CA);
  client.setTimeout(12000);
  return true;
}

上面的证书内容必须替换成服务端证书链对应的 CA,不能把示例文本直接烧录。证书有多个中间 CA 时,要根据服务器配置和客户端信任链决定放哪一张。用浏览器查看证书链只能帮助定位,最终仍应以服务端文档和证书管理策略为准。

五、发起 HTTPS GET 请求

bool getJson(const char* host, uint16_t port, const char* path) {
  if (!client.connect(host, port)) {
    Serial.println("TLS connect failed");
    return false;
  }
  client.printf("GET %s HTTP/1.1\\r\\n", path);
  client.printf("Host: %s\\r\\n", host);
  client.println("User-Agent: esp32-client/1.0");
  client.println("Connection: close");
  client.println();

  uint32_t start = millis();
  while (!client.available() && millis() - start < 12000) delay(10);
  if (!client.available()) {
    client.stop();
    return false;
  }

  while (client.connected() || client.available()) {
    String line = client.readStringUntil('\\n');
    Serial.println(line);
  }
  client.stop();
  return true;
}

示例为了教学直接逐行打印响应,生产代码要限制每行长度,解析 HTTP 状态码和 Content-Length,并根据 JSON 内容判断业务是否成功。服务端返回 200 不代表业务一定完成,可能还要检查 success 字段或任务编号。

六、重试和退避

TLS 失败可能来自 DNS、路由器、服务器过载、证书更新或内存不足。重试应区分错误:证书不可信时盲目重试没有意义;网络暂时不可用时可以采用指数退避,例如 2、4、8、16 秒,并设置最大等待。

bool requestWithRetry(const char* host, const char* path) {
  const uint32_t backoff[] = {2000, 4000, 8000};
  for (size_t i = 0; i < 3; ++i) {
    if (getJson(host, 443, path)) return true;
    delay(backoff[i]);
  }
  return false;
}

设备上报数据时最好使用幂等接口或带唯一事件 ID,避免客户端不知道服务器是否收到时重复创建订单、重复执行动作。网络工程里,“重试”从来不是一句简单的 for 循环。

七、为什么不建议 setInsecure

setInsecure() 会关闭证书验证,适合实验室临时定位“是不是 TLS 握手本身的问题”,不适合生产。中间人可以伪造服务器,网络中的其他设备也能读取和修改数据。尤其是传感器上报密码、控制指令或个人信息时,这个风险不是理论上的。

调试时可以打印 client.lastError() 或启用更详细的日志,确认问题后恢复 CA 校验。不要为了让演示顺利就把不安全配置提交到代码仓库,几个月后它很容易一路进入量产固件。

八、证书过期怎么办

证书会过期,根 CA 也可能发生轮换。固件需要把证书更新当成产品维护流程:提前监控服务端有效期,准备双证书或远程更新机制,更新失败时保留旧证书;如果用 OTA 更新证书,也必须验证新文件的签名和来源。

设备时间不准也会造成“偶发过期”。记录当前时间、服务器域名和 TLS 错误码,运维人员才能区分是校时失败还是证书真的到期。不要只在日志里留下一个模糊的“request failed”。

九、总结

ESP32 做 HTTPS 客户端并不难,难的是把校时、证书、超时、重试、内存和证书生命周期都纳入设计。推荐的顺序是:连接 Wi-Fi,校准时间,加载 CA,建立 TLS,解析响应,按错误类型退避重试。只有在明确的开发调试阶段,才短暂使用不安全模式定位问题。

当这条链路稳定后,前面做过的温湿度、GPS、摄像头、CAN 和 Web 项目都可以安全地把数据送到云端。安全连接不是额外装饰,而是让“能联网”真正升级为“可以放心联网”。

在产品测试阶段,可以故意准备几种失败场景:把设备时间拨到证书生效前、断开路由器、让 DNS 解析失败、替换成错误 CA、让服务器延迟返回。观察每种错误是否都能在有限时间内退出,日志里是否能区分原因,重试是否会逐渐放慢。网络安全代码最怕只测试成功路径,因为真实设备的大部分时间都在面对不稳定的网络。

还要给 HTTPS 请求设置业务级超时。底层 socket 超时只能保证连接不会永久卡住,不能保证服务器返回的 JSON 一定完整,也不能保证数据没有重复提交。上报接口最好带设备序列号、事件序号和时间戳,服务器端做去重。

参考资料

打赏

如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏码

打赏支持:王庆岩

posted on 2026-09-30 10:18  潇洒石头  阅读(24)  评论(0)    收藏  举报

导航