ESP32用HTTPS获取天气全流程:证书校验与踩坑实战

发布时间:2026/9/9 13:25:12
ESP32用HTTPS获取天气全流程:证书校验与踩坑实战 简介这是一份面向物联网开发者的ESP32安全通信示例演示如何通过HTTPS协议获取天气数据。工程借助mbedtls库建立加密连接并使用cJSON库处理接口返回的数据步骤涵盖无线网络配置、请求构造、响应接收及信息提取压缩包内共有一千六百余个文件主要包括编译目标文件、依赖关系文件、头文件、构建脚本、静态库以及开发框架配置文件其中包含的库文件有助于理解底层实现压缩包整体大小约为十九兆字节目前已有五千余人学习下载工程中的构建文件明确了编译规则配置文件保存了开发框架选项可辅助快速搭建项目。示例为简化流程未验证服务器证书实际部署时应加入证书链校验以防通信被截获或伪造读者可在此基础上完善安全机制通过该示例可掌握无线网络初始化、请求发送、数据解析等关键技能为后续开发定位或传感上报等应用打下基础。 上个月我把客厅那个用了两年的温湿度计拆开换成了ESP32驱动的一块OLED小屏幕。原本只是想在屏上顺带显示一下室外天气结果第3天就发现问题了——凌晨2点屏幕上的温度数值变成了一个广告链接再刷新直接白屏。查了一圈日志问题出在我用的还是HTTP明文请求响应内容被网络链路里的某个环节改了包。这其实不是个例。ESP32这类物联网设备做HTTP请求实在太顺手了很多人包括我一开始都会忽略加密这事。但如果你做的天气盒子、传感器网关、远程控制面板需要联网拿数据HTTPS真不是可有可无的加分项而是必须迈过去的坎。这篇我打算用ESP32通过HTTPS获取天气这个小DEMO当引子把从证书到代码再到调试踩坑的完整链路讲明白适合刚玩ESP32没多久、想在项目里接API但不想交明文学费的朋友。1. 为什么一个天气DEMO非要折腾HTTPS1.1 HTTP明文请求的代价被劫持的天气数据先说我那个白屏的教训。HTTP报文是纯文本传输请求和响应在链路上任何一个环节都能被拆开看、改掉再重新封装。你的ESP32向天气服务器要数据这条数据链路可能经过路由器、运营商网关、CDN节点中间哪个环节出了毛病或者被夹带私货返回给你的JSON就已经不是原来那串了。当时我遇到的情况就是响应正常返回但JSON里被人塞进了一段HTML脚本ArduinoJson解析到一半直接报语法错误程序崩溃重启。如果你做的是真正控制类的设备——比如智能插座、远程开关明文请求被篡改的后果就远不止白屏了这是我在实际项目里坚持上HTTPS的最根本原因。1.2 HTTPS在ESP32上的三个真实难点HTTPS本身的原理不复杂建立TCP连接后先做TLS握手协商密钥、校验证书之后所有数据对称加密传输。但这件事放到ESP32上有三道坎和PC上完全不一样。第一道坎是证书校验。浏览器里系统帮你维护了一整套根证书库ESP32出厂可没有。WiFiClientSecure要么你把服务器证书的根证书硬编码进固件要么就需要关闭证书验证——后者虽然写起来省事但等于把加密通信降级成了只防偷听不防冒充我建议能不用就不用。第二道坎是系统时间。TLS证书校验有个关键步骤是检查有效期。ESP32刚上电时RTC时间是1970年证书校验证书还没生效呢直接失败。所以必须先通过NTP同步时间再去发起HTTPS请求。第三道坎是内存。ESP32可用RAM也就300多KBTLS握手过程本身要消耗连接缓冲加上JSON解析、HTTP响应缓存几个大块的内存叠一起新手很容易遇到重启或者分配失败。这些问题我会在第4章展开讲。2. 环境与依赖先把证书和库选明白2.1 开发环境与核心库选型我用的开发环境是Arduino IDE搭配espressif官方ESP32开发板包版本2.x以上都行。如果你习惯PlatformIO也没问题下面代码可以直接移植核心只是库的选择。这个DEMO涉及三个基础库都直接随板包自带了WiFi.h连接WiFi属于基础网络功能WiFiClientSecure.h提供TLS加密传输能力是HTTPS的核心HTTPClient.h封装HTTP协议层的请求、响应处理ArduinoJson用来解析天气API返回的JSON数据这个需要你在库管理器里单独搜一下安装需要说明的是ArduinoJson推荐装最新的7.x版本。6.x和7.x在API上有差异主要是DynamicJsonDocument被JsonDocument取代了如果你搜到的老教程用的是6.x写法注意替换。2.2 天气数据源选择做DEMO最怕注册账号、申请Key、配权限这一堆前置工作。当初我选天气源就俩要求一是支持HTTPS二是最好不要API Key能让读者拿到代码直接跑。Open-Meteo是一个完全免费、无需注册的天气服务聚合了全球多个气象模型数据通过api.open-meteo.com提供HTTPS接口。返回格式是标准JSON非常适合做这个DEMO。下面是它的一个最简请求路径https://api.open-meteo.com/v1/forecast?latitude31.23longitude121.47current_weathertruetimezoneAsia%2FShanghai这段参数的意思是请求北纬31.23、东经121.47位置的当前天气时区按上海处理。返回的数据长这样节选一下关键字段{ current_weather: { temperature: 21.5, windspeed: 12.3, time: 2024-01-15T10:00 } }如果你想接和风天气这类国内服务商原理完全一样只是需要在请求头加上你的API KeyTLS部分不需要改动。2.3 根证书的获取与配置这一步是HTTPS连接能不能成功的关键。WiFiClientSecure在默认情况下不信任任何证书你需要把服务器的根证书写进代码里。获取根证书有两种常用方法。第一种是用OpenSSL命令直接抓取openssl s_client -showcerts -connect api.open-meteo.com:443 -servername api.open-meteo.com运行后会输出证书链把最顶层的那个BEGIN CERTIFICATE到END CERTIFICATE之间整段复制出来即可。Open-Meteo用的证书链最终根证书是ISRG Root X1也就是Lets Encrypt的根证书。第二种方法更省事直接从MySQL官方提供的CA证书页下载cacert.pem里面包含了全球主流公共根证书然后从文件中找到ISRG Root X1那一段。这个文件比较长但ESP32的Flash存一段字符串毫无压力。我在代码里把根证书定义成常量const char* rootCACert REOF( -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw ... -----END CERTIFICATE----- )EOF;用原生字符串REOF(...)包裹好处是证书里的换行符不用转义直接粘贴。有了证书之后还要理解一个问题为什么只给根证书就能校验服务器证书这是TLS证书链的工作原理。服务器在握手中会下发自己的证书和中间证书客户端拿到后用内置的根证书去校验整条链路的签名只要链条完整且签名合法就认为服务器身份可信。ESP32的WiFiClientSecure内部实现了这套链式校验逻辑所以只要根证书正确不需要在代码里逐个配中间证书。3. 代码拆解从连接WiFi到解析天气JSON3.1 连接WiFi与NTP时间同步WiFi连接这部分大家应该很熟了我只提一个容易被忽略的点连接成功后不要立刻发起HTTPS请求给网络栈一点稳定时间简单加个几百毫秒延时即可。重点讲NTP时间同步。前面说了证书有效期校验依赖系统时间所以必须在TLS握手之前把时间拉准。ESP32上用configTime函数配置NTP服务器一行代码搞定configTime(8 * 3600, 0, ntp.aliyun.com, pool.ntp.org);第一个参数是时区偏移秒数东八区是8小时所以传8 * 3600第二个参数是夏令时偏移国内不需要传0后面跟的是NTP服务器地址阿里云的和公网的各配一个提高解析成功率。配置完之后用循环等待时间同步完成while (time(nullptr) 100000) { delay(500); Serial.print(*); }这个条件很有意思。time(nullptr)返回1970年1月1日到现在的秒数当它小于10万的时候说明还是1970年附近同步没完成。一旦同步成功这个值会跳到当前时间戳的十位数级条件自然退出。这个写法是我从国外一个开源项目里学的比用getLocalTime做NULL判断再配合超时计时要简洁得多。3.2 HTTPS请求的建立与发送时间同步完成后创建安全的WiFi客户端并挂载根证书WiFiClientSecure secureClient; void setup() { // ... WiFi连接、NTP同步代码 ... secureClient.setCACert(rootCACert); }接下来是核心部分用HTTPClient发起GET请求。这里有一个容易踩的坑HTTPClient的begin()方法有好几个重载连接HTTPS时要用带主机名、端口、路径和证书校验开关的完整版本#include HTTPClient.h bool fetchWeather() { HTTPClient http; String path /v1/forecast?latitude31.23longitude121.47current_weathertruetimezoneAsia%2FShanghai; // 关键这里用的是WiFiClientSecure实例 // 而不是默认的无加密客户端 http.begin(secureClient, api.open-meteo.com, 443, path, true); int httpCode http.GET(); if (httpCode 0) { Serial.printf(HTTP code: %d\n, httpCode); if (httpCode HTTP_CODE_OK) { String payload http.getString(); http.end(); return parseWeather(payload); } } else { Serial.printf(HTTPS request failed, error: %s\n, http.errorToString(httpCode).c_str()); } http.end(); return false; }begin函数最后那个true参数含义是启用证书校验。如果你把这个参数改成false就算setCACert设了根证书也不会校验。这个参数容易忽略我有一阵子总是连不上查了半天才发现是这里写错了。还有一点http.begin传入WiFiClientSecure对象时HTTPClient并不会复制这个对象而是持有引用。所以你的secureClient必须在整个HTTP会话期间保持存活不能放在局部函数里定义然后立刻释放否则会出现奇怪的崩溃或者连接失败现象。另外HTTPClient的实例在每次请求后要调用http.end()释放连接资源否则多次请求后连接池会耗尽表现为前几次正常后面请求全部超时。这个坑我在写循环刷新功能时也踩过。3.3 JSON解析与数据提取响应拿到手后就是一个JSON字符串。用ArduinoJson把这个字符串解析成文档对象再用层级路径访问天气数据非常直观#include ArduinoJson.h bool parseWeather(const String payload) { JsonDocument doc; DeserializationError error deserializeJson(doc, payload); if (error) { Serial.println(JSON parse failed); return false; } float temperature doc[current_weather][temperature]; float windSpeed doc[current_weather][windspeed]; Serial.printf(Temperature: %.1f C\n, temperature); Serial.printf(Wind speed: %.1f km/h\n, windSpeed); // 这里可以把数据存到全局变量供loop循环中刷新显示 return true; }ArduinoJson 7.x在内存使用上相比6.x优化了不少JsonDocument可以视作一个自动管理内存的容器。不过有一点要注意deserializeJson解析时JsonDocument内部会动态分配堆内存。如果JSON字符串超大或者内存碎片严重分配失败会返回NoMemory错误。对这种极端情况可以在返回前检查一下error类型打印日志方便排查。关于内存如果你项目里同时开了TLS和JSON解析最好在关键节点用ESP.getFreeHeap()打印一下可用堆内存。实测下来这个DEMO在完整跑完一次HTTPS请求JSON解析后空闲堆内存大约从170KB降到120KB左右属于正常范围。如果低到20KB以下基本就要调整设计或者拆分请求了。4. 真机调试备忘录我的失败经验与排查链路这一章是我最想写的部分。代码本身不难难的是第一次烧录后设备不按你想的跑。下面三条是我在这个DEMO上真实遇到过的故障现象每条都附上了我的排查链路你可以直接当调试手册用。4.1 证书验证失败连接被重置现象是串口打印HTTPS request failed, error: connection refused或者SSL error但同样的代码换成HTTP就正常。先别急着怀疑代码逻辑。我的排查链路是这样的第一步确认系统时间是否同步成功。时间不同步时证书有效期校验必然失败。我在代码里加入这样一段临时调试代码把当前时间打出来time_t now time(nullptr); struct tm* timeInfo localtime(now); Serial.printf(Current time: %s, asctime(timeInfo));如果打印出来还是1970年问题就出在NTP同步环节。常见原因包括路由器屏蔽了UDP 123端口、NTP服务器域名解析失败、或者是WiFi刚连上时网络栈还没准备好。可以换一个NTP服务器试试比如time.nist.gov或者等待时间加长。第二步确认证书内容完整。我犯过一个很低级的错误从OpenSSL输出里复制证书时只复制了中间证书根证书没复制完整。TLS握手时服务器如果发现客户端校验失败会直接重置连接。把代码里的rootCACert完整打印出来和抓取结果比对一遍这一步花不了两分钟。第三步测试证书校验开关。把http.begin最后一个参数临时改成false如果这样能连上说明TLS握手本身没问题纯粹是证书配置有误。这时候回头检查证书链而不是怀疑网络。4.2 HTTP 404与未知错误状态第二种常见现象是请求发出去了但返回404。这通常和证书没关系要么是路径写错要么是主机名和路径组合有问题要么是URL里的特殊字符没有编码。比如Open-Meteo的timezoneAsia/Shanghai斜杠在URL里是特殊字符必须编码成%2F。我最初直接填Asia/Shanghai请求返回400和404的概率很高因为服务器解析参数时把Asia/Shanghai当成了两段路径。排查这类问题有个好办法直接把请求路径在电脑浏览器里打开对比返回结果。浏览器通过GET方式访问就等同于你在ESP32上发起的请求如果浏览器能正常返回JSON而ESP32返回404问题一定在路径编码或者请求头设置上。还有一种情况是返回了unknown error。串口打印这个信息时HTTPClient没拿到有效的HTTP状态码通常是TCP层就被断了。这时候去检查WiFi信号强度和网络稳定性。ESP32的WiFi在弱信号下容易出现TCP连接中断表现为偶发性的请求失败。我是通过增加一个简单的重试机制解决的最多重试3次每次间隔2秒成功率立刻上来了。4.3 内存与稳定性陷阱第三个问题相对隐蔽表现为设备运行几小时或者几天后某次请求突然失败然后反复重启。ESPS32的堆内存不是无限的。每次HTTPS请求建立TLS连接时WiFiClientSecure内部会分配加密上下文这个内存大约占30-40KB。如果http.end()没有调用或者WiFiClientSecure对象被反复创建销毁产生大量碎片空闲堆会越来越小直至分配失败导致重启。我用的应对方案有三个一是复用WiFiClientSecure对象。在全局只创建一次通过setCACert设置证书之后每次请求前调用stop()清理上一次的连接状态而不是反复创建新对象。二是排查全局变量与字符串拼接。尽量避免用String类做大量拼接操作它在堆上频繁分配释放会产生碎片。我改成了snprintf格式化到char数组内存占用变得非常可控。三是加上软件看门狗。当连续请求失败超过5次主动调用ESP.restart()重启避免设备卡死在半死不活的状态。做远程设备时这招特别好用至少不用总想着去拔电源。5. 从DEMO到能用稳定性与扩展设计5.1 一个朴素但可靠的定时刷新策略DEMO里loop循环最简单的方法就是delay但这有个问题如果请求耗时超过delay时间实际上刷新周期会漂移。我用了一种稍微正式一点但依然很朴素的做法——基于毫秒时间戳的非阻塞调度unsigned long lastFetchTime 0; const unsigned long FETCH_INTERVAL 30UL * 60 * 1000; // 30分钟 void loop() { if (millis() - lastFetchTime FETCH_INTERVAL) { bool ok fetchWeather(); if (ok) { lastFetchTime millis(); } } delay(1000); }设置30分钟刷新一次对天气这种变化不频繁的数据完全够用。millis()溢出的问题在这个量级基本不用考虑如果真想严谨可以用(millis() - lastFetchTime)这种差值运算写法而不是直接比较当前时间和目标时间这样能天然处理溢出问题。真正要注意的是失败重试策略。如果某次请求失败上面的代码不会更新lastFetchTime导致下个循环立刻重试这其实是合理的。但为了防止网络连续故障时设备高频自旋请求我在fetchWeather里加了一个失败计数连续失败5次后进入60秒锁定不再发请求直到WiFi状态恢复。5.2 显示方案与低功耗延伸这个DEMO获取到的天气数据最终可以给很多东西用。我最初是输出到OLED屏如果你用的是SSD1306128x64像素就够显示一行温度加一个天气图标。用U8g2库驱动OLED是常见选择在parseWeather里把数据存成全局变量loop刷新显示即可。如果想做得更省电一点核心思路是让ESP32大部分时间都处于深度睡眠只在需要刷新天气时定时唤醒。ESP32的esp_sleep_enable_timer_wakeup配合esp_deep_sleep函数可以做到微安级别的待机功耗。唤醒后跑一次WiFi连接HTTP请求完事继续睡。如果用锂电池供电这样能把设备续航从一天拉长到一个月以上。我不建议在DEMO阶段直接上低功耗先保证功能稳定再加这个优化调试起来会容易很多。还有一个扩展方向是同时请求多个城市或者多类型数据。Open-Meteo支持经纬度数组查询你可以用一条请求拿到多个城市的天气内存上花的代价比发多条请求小得多。具体返回格式可以查Open-Meteo官方文档路径参数加个latitude31.23,39.90longitude121.47,116.40就能实现。我在实际做这个项目的时候最初也嫌HTTPS麻烦差点就直接用HTTP凑合了。但经过那次被篡改数据的事情之后我意识到加密通信不应该被当成进阶功能——只要你做的设备需要联网、需要处理真实数据这就是底线。调试TLS头两天确实烦人但一旦把证书流程理顺后面换任何HTTPS API都只是改改域名和路径的事情。希望这篇能把你的第一道坎提前填平少走几个我走错的路口。本文还有配套的精品资源点击获取