设备上电后以 STA 模式接入无线网络“航天科宇”,使用固定地址 192.168.10.230 提供服务,在 80 端口启动 HTTP 服务。访问根路径时必须通过 HTTP Basic 认证:认证成功返回欢迎页面,认证失败则在登录弹窗中提示“密码错误,请重新登录”。设备同时开启 Arduino OTA,支持不插 USB 的无线固件升级。
成功页面共三行内容,依次为“登录成功”“欢迎进入EEPW世界”和“作者:keyboard007”,整体在浏览器窗口中居中显示。
1.3 运行环境
硬件环境为 M5Stack AtomS3R 开发板,板载 ESP32-S3,支持 2.4GHz 频段无线网络。
软件环境为 Arduino IDE 或 arduino-cli,配套 esp32 core 3.3.9。
编译参数:开发板选择 M5AtomS3,对应的 FQBN 为 esp32:esp32:m5stack_atoms3;上传端口为 COM15;串口监视器波特率为 115200。
访问方式:在同一网段的电脑浏览器中打开 http://192.168.10.230/。
1.4 账号说明
凭据表内置五组账号,分别为 admin/esp32、fred/41234123、charlie/sdfsd、alice/vambdnkuhj、bob/svcdbjhws12。账号以结构体数组的形式保存在固件中,新增或删除账号只需修改数组内容,条目数量由 sizeof 自动计算,不必改动其他逻辑。
第二章 设计思路
2.1 总体架构
程序自上而下分为四层。网络连接层负责 WiFi 接入与静态地址配置;服务层由 WebServer 监听 80 端口并分发请求;认证层通过回调函数完成用户名到密码的映射;渲染层统一生成带样式的 HTML 页面。四层之间通过函数调用串联,结构简单,出现问题时可以逐层定位。
2.2 认证方案选择
WebServer 库提供两类认证接口:一类是固定用户名密码的 authenticate(username, password),只能校验单一账号;另一类是带回调的 authenticate(函数指针),适合多用户场景。回调签名为 String *(HTTPAuthMethod mode, String username, String params[]),返回该用户名对应的密码指针,返回 NULL 表示用户不存在。
真正的 base64 解码、密码比对、恒定时间比较以及返回值的内存释放都由库内部完成,业务代码只承担“查表”这一件事,职责清晰,也不容易写出内存泄漏或二次释放。返回类型之所以用指针而非字符串,是为了多表达一种“查无此人”的状态,这是回调式认证与固定账号认证在设计上的关键差别。
2.3 凭据表设计
用 typedef struct 定义 credentials_t,包含 username 与 password 两个 String 成员;再用数组集中保存全部账号;最后用 sizeof(passwdfile) / sizeof(credentials_t) 计算条目数。这样增删账号不会遗漏循环边界,也避免了手写数量与内容不一致的常见错误。
2.4 错误提示设计
HTTP Basic 认证由浏览器负责弹出登录框,在弹窗显示期间页面正文不会渲染,因此“密码错误”的提示只能借助响应头 WWW-Authenticate 中的 realm 字段传递。程序根据请求是否携带 Authorization 头区分两种情形:首次访问使用 realm “EEPW”;带了凭据却验证失败,则使用 realm “EEPW 密码错误,请重新登录”,浏览器会把这段文字显示在弹窗的消息区域。
与此同时,401 响应正文还放置了一个居中的“密码错误”页面。当用户点击弹窗的取消按钮时,浏览器会直接渲染这段正文,形成第二重提示。两种提示覆盖了“继续重试”和“放弃登录”两种用户行为。
2.5 页面设计
所有页面由 htmlPage(title, content) 统一生成。页面使用 flex 布局,使白色卡片在窗口中水平与垂直方向同时居中,卡片内部文字统一居中,并通过圆角与浅阴影提升观感。登录成功页包含三行内容:登录成功、欢迎进入EEPW世界、作者:keyboard007;其中作者行使用较小的灰色字体,与主标题形成层次,不喧宾夺主。
2.6 网络与升级设计
设备使用静态 IP,避免因 DHCP 租期到期或路由器重启导致地址变化。静态配置必须写在 WiFi.begin() 之前,否则不会生效。ArduinoOTA 在联网成功后启动,主循环持续调用其 handle 方法,使设备既能通过串口烧录,也能在局域网内无线升级。
第三章 程序流程图与代码编写步骤
3.1 启动流程
上电复位
│
├─ Serial.begin(115200)
├─ WiFi.mode(WIFI_STA)
├─ WiFi.config(192.168.10.230, 网关, 掩码, DNS)
├─ WiFi.begin(SSID, 密码)
│ ├─ 连接失败 → 打印提示 → 延时 1 秒 → ESP.restart()
│ └─ 连接成功
├─ ArduinoOTA.begin()
├─ server.on("/", 处理函数)
├─ server.begin()
└─ 串口打印访问地址 http://192.168.10.230/
主循环 loop()
├─ ArduinoOTA.handle()
├─ server.handleClient()
└─ delay(2) 让出 CPU 给 WiFi 任务3.2 请求处理流程
浏览器发起 GET / │ ├─ 请求头中没有 Authorization │ └─ 返回 401,realm = EEPW,正文为“需要登录”页 │ 浏览器弹出登录框,用户输入账号密码 │ └─ 请求头中带有 Authorization ├─ 库解析 Basic 凭据,取出用户名 ├─ 调用 credentialsHandler 查表 │ ├─ 用户不存在或密码不符 │ │ 返回 401,realm = EEPW 密码错误,请重新登录 │ │ 弹窗显示错误提示,正文为“密码错误”页 │ └─ 密码匹配 │ 返回 200,渲染“登录成功”页 └─ 单次请求处理结束,等待下一次请求
3.3 代码编写步骤
(1)引入 WiFi.h、ESPmDNS.h、ArduinoOTA.h 与 WebServer.h 四个头文件。
(2)定义无线网络名称与密码,并创建监听 80 端口的 WebServer 对象。
(3)定义凭据结构体 credentials_t,建立账号数组,并用 sizeof 计算条目数。
(4)实现认证回调 credentialsHandler,遍历数组匹配用户名,返回密码指针或 NULL。
(5)实现页面生成函数 htmlPage,封装页面骨架、字体与居中样式。
(6)编写 setup 函数:初始化串口,切换 STA 模式,配置静态 IP,连接无线网络,判断连接结果,启动 OTA,注册根路径路由,启动服务并打印访问地址。
(7)编写 loop 函数:轮询 OTA 与 HTTP 客户端请求,并用 delay(2) 让出 CPU。
(8)编译烧录后,在浏览器中依次验证首次访问、密码错误、密码正确三种场景。
第四章 程序代码
完整源文件名为 06HttpAuthCallback.ino,全部代码如下:
#include <WiFi.h>
#include <ESPmDNS.h>
#include <ArduinoOTA.h>
#include <WebServer.h>
const char *ssid = "航天科宇";
const char *password = "88888888";
WebServer server(80);
typedef struct credentials_t {
String username;
String password;
} credentials_t;
credentials_t passwdfile[] = {
{"admin", "esp32"}, {"fred", "41234123"}, {"charlie", "sdfsd"}, {"alice", "vambdnkuhj"}, {"bob", "svcdbjhws12"},
};
const size_t N_CREDENTIALS = sizeof(passwdfile) / sizeof(credentials_t);
String *credentialsHandler(HTTPAuthMethod mode, String username, String params[]) {
for (int i = 0; i < N_CREDENTIALS; i++) {
if (username == passwdfile[i].username) {
return new String(passwdfile[i].password);
}
}
return NULL;
}
// 生成统一风格的页面,内容在窗口正中显示
String htmlPage(const String &title, const String &content) {
String page = "<!DOCTYPE html><html><head><meta charset='utf-8'>";
page += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
page += "<title>";
page += title;
page += "</title>";
page += "<style>html,body{height:100%;margin:0}";
page += "body{display:flex;align-items:center;justify-content:center;background:#f5f6f7;";
page += "font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif}";
page += ".card{padding:32px 40px;background:#fff;border-radius:12px;";
page += "box-shadow:0 4px 20px rgba(0,0,0,.08);text-align:center}";
page += "h1{margin:0 0 12px;font-size:24px;color:#1a1a1a}";
page += "p{margin:0;font-size:16px;color:#555}";
page += ".author{margin-top:8px;font-size:14px;color:#888}</style></head>";
page += "<body><div class='card'>";
page += content;
page += "</div></body></html>";
return page;
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
// 固定 IP:地址 192.168.10.230,网关 192.168.10.1,掩码 255.255.255.0
IPAddress localIP(192, 168, 10, 230);
IPAddress gateway(192, 168, 10, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress dns(192, 168, 10, 1);
WiFi.config(localIP, gateway, subnet, dns);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("WiFi Connect Failed! Rebooting...");
delay(1000);
ESP.restart();
}
ArduinoOTA.begin();
server.on("/", []() {
// 请求里带了 Authorization 头却认证失败,说明用户名或密码错误
bool sentCredentials = server.hasHeader("Authorization");
if (!server.authenticate(&credentialsHandler)) {
if (sentCredentials) {
server.requestAuthentication(
BASIC_AUTH, "EEPW 密码错误,请重新登录",
htmlPage("EEPW", "<h1>密码错误</h1><p>请重新输入正确的用户名和密码</p>"));
} else {
server.requestAuthentication(
BASIC_AUTH, "EEPW",
htmlPage("EEPW", "<h1>需要登录</h1><p>请输入用户名和密码</p>"));
}
return;
}
server.send(200, "text/html; charset=utf-8",
htmlPage("EEPW", "<h1>登录成功</h1><p>欢迎进入EEPW世界</p>"
"<p class='author'>作者:keyboard007</p>"));
});
server.begin();
Serial.print("Open http://");
Serial.print(WiFi.localIP());
Serial.println("/ in your browser to see it working");
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
delay(2); //allow the cpu to switch to other tasks
}

第五章 调试注意事项
5.1 编译与烧录
开发板必须选择与硬件对应的 FQBN,否则可能因 Flash 容量或分区表差异导致烧录后无法正常启动。本项目使用的是 esp32:esp32:m5stack_atoms3。
烧录前要关闭 Arduino IDE 的串口监视器,否则串口被占用,esptool 会报端口繁忙(PermissionError 13)并终止。串口监视器进程名通常为 serial-monitor,必要时可以先结束该进程再烧录。
编译与烧录过程中,工具链会在系统临时目录生成中间文件,若权限受限会导致编译中断,需要以具备写权限的账户运行。
5.2 网络与地址
静态 IP 必须与网关处于同一网段,并且是当前未被占用的地址。配错网关或掩码会导致设备虽然连上无线网络却无法访问,只能重新用 USB 烧录恢复,因此修改地址后应先用 ping 验证。
本项目对地址安全性做过一次实测:临时去掉静态配置改走 DHCP,设备仍然获得 192.168.10.230。这说明该地址要么位于路由器的可分配范围,要么路由器对本机 MAC 做了保留,两种情况短期内都不会冲突。为彻底排除隐患,仍建议在路由器后台确认地址池范围,或直接按 MAC 做静态绑定。
5.3 浏览器行为
浏览器会缓存 Basic 认证凭据,修改密码后可能在没有任何提示的情况下继续使用旧凭据,验证失败场景时应使用无痕窗口或更换访问路径。
realm 中的中文由浏览器负责渲染,个别浏览器可能显示为乱码。若出现该现象,把 realm 换成英文提示即可,页面正文中的中文不受影响。
5.4 中文与编码
页面必须声明 charset=utf-8,否则中文会显示为乱码;源文件本身也应保存为 UTF-8 编码,避免编辑后重新编译出现乱码。
5.5 安全性
Basic 认证只做 base64 编码而非加密,凭据可被抓包还原,仅适用于可信内网。密码硬编码在固件中,程序也没有失败次数限制和锁定机制,因此不适合直接当作公网登录系统使用。
5.6 无线升级
OTA 与串口烧录共用同一套升级机制,升级过程中不要断电;设备与电脑需要处于同一网段,否则网络端口无法被工具发现。
第六章 调试心得
第一,理解回调式认证的返回值语义是关键。返回密码指针而不是字符串,使“用户不存在”与“密码为空”两种状态得以区分;内存释放交给库处理,自己再 delete 反而会造成二次释放。这一点在写代码之前想清楚,可以省去很多排查时间。
第二,realm 可以当作提示通道使用,但要清楚它的边界:提示文字由浏览器渲染,页面正文在弹窗期间不可见。若对提示样式和交互有更高要求,正确做法是把认证换成自建登录表单与会话机制,而不是继续在响应头上做文章。
第三,推断不能代替实测。最初根据局域网扫描结果推断 DHCP 池为 100 到 199 段,认为 230 属于静态保留区;实际把静态配置去掉后设备依然拿到 230,说明推断有误。用一次真实测试推翻错误假设,比反复推理更高效。
第四,调试要分层。先用串口确认联网成功并看到 IP,再用 ping 与 ARP 确认链路可达、MAC 正确,最后用浏览器验证认证逻辑。这样出现问题时能迅速判断是网络层、服务层还是业务层的问题。
第五,改进方向已经比较明确。凭据可以迁移到 NVS 或加密分区;可以增加失败次数锁定与访问日志;对外网部署应改用 HTTPS;多页面场景可结合 Cookie 会话与分级权限。当前版本规模很小,但结构清晰,便于在此基础上继续扩展。
我要赚赏金
