使用ESP32自动登录需要WEB页面认证的WIFI网络
在很多大型WIFI网络环境中,连接网络不仅需要热点的密码,还需要其他的认证手段,即2次认证。常见的场景比如:连接上热点后,手机或电脑会自动弹出一个认证页面,需要你输入用户名密码,或者通过手机验证码进行再次认证。比如这种:通过认证之后,设备才能正常访问互联网。那么我们如果ESP32,的话,如何去登录这种网络呢?
一、背景
在很多大型WIFI网络环境中,连接网络不仅需要热点的密码,还需要其他的认证手段,即2次认证。
常见的场景比如:连接上热点后,手机或电脑会自动弹出一个认证页面,需要你输入用户名密码,或者通过手机验证码进行再次认证。
比如这种:


通过认证之后,设备才能正常访问互联网。
那么我们如果ESP32,的话,如何去登录这种网络呢?
二、前端常见的WEB登录方式
前端WEB页面在登录方式上,因为不同的厂商不同的开发人员,页面代码上对登录的处理也差异很大。
但归根到底,提交登录信息的方式无非只有两种:GET和POST。
GET方式很少,因为用户输入的username和password会直接暴露在URL中,所以一般还是用POST方法。
POST方法又分为很多种,常见的有:表单form提交、java-script提交。其中java-script提交又可能会使用Ajax、fetch API等。
所以,我们的目的是,利用代码去自动分析当前的网络环境使用的是什么登录方法,并找出HTML页面,或者java-script中的提交登录信息的方法,最终使ESP32实现自动登录。
三、头文件WifiAuth.h
#ifndef WIFI_AUTH_H
#define WIFI_AUTH_H
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
class WiFiAuth {
public:
enum class DataFormat {
UNKNOWN,
FORM_URLENCODED,
JSON
};
// 构造函数
WiFiAuth(const char* authUrl = nullptr, const char* fieldUser = nullptr, const char* fieldPass = nullptr);
// 核心功能
bool checkRedirect();
bool submitAuth(const String& username, const String& password);
bool isAuthPageContent(const String& content);
String getLastError() const;
bool fetchAndParseAuthPage(const String& url);
bool parseMetaRefresh(const String& payload);
bool parseTraditionalForm(const String& html);
void parseJSAjaxSubmit(const String& html);
String findMainJS(const String& html);
String parseAjaxUrlFromJS(const String& jsContent);
String parseFieldFromJS(const String& jsContent, const String& fieldType);
String fetchJSContent(const String& jsUrl);
String resolveRelativeUrl(const String& url) ;
String getBaseUrl(const String& fullUrl) ;
bool testJsUrlExists(const String& url) ;
DataFormat detectDataFormat(const String& jsContent);
private:
String _authUrl;
String _formAction;
String _fieldUser;
String _fieldPass;
String _lastError;
// 辅助方法
bool _parseMetaRefresh(const String& html);
bool _parseAuthPage(const String& html);
bool _parseAuthPageFromUrl(const String& url);
String extractFieldName(const String& html, const String& pattern);
String urlEncode(const String& str);
String getDomainFromUrl(const String& url);
DataFormat _dataFormat;
};
#endif
四、源文件
#include "WiFiAuth.h"
WiFiAuth::WiFiAuth(const char* authUrl, const char* fieldUser, const char* fieldPass) :
_authUrl(authUrl ? authUrl : "http://1.1.1.1/login"),
_fieldUser(fieldUser ? fieldUser : "username"),
_fieldPass(fieldPass ? fieldPass : "password") {
_lastError.reserve(128);
_formAction.reserve(64);
}
bool WiFiAuth::checkRedirect() {
// 初始化HTTP客户端并配置
WiFiClient client;
HTTPClient http;
http.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
http.setTimeout(3000);
http.setUserAgent("ESP32-WiFiAuth");
// 尝试连接目标网站
if (!http.begin(client, "http://www.baidu.com")) {
_lastError = "HTTP连接初始化失败";
return false;
}
// 执行GET请求并获取响应信息
int httpCode = http.GET();
String location = http.getLocation();
bool isHttpSuccess = (httpCode == HTTP_CODE_OK);
String payload = isHttpSuccess ? http.getString() : "";
http.end();
/* 情况1:处理HTTP重定向
* - 状态码为302/307
* - Location头不为空
* - 重定向目标不是百度域名 */
bool isHttpRedirect = (httpCode == HTTP_CODE_FOUND || httpCode == HTTP_CODE_TEMPORARY_REDIRECT);
if (isHttpRedirect &&
!location.isEmpty() &&
location.indexOf("baidu.com") == -1) {
_authUrl = location;
return fetchAndParseAuthPage(_authUrl);
}
/* 情况2:处理HTML meta refresh重定向
* - 状态码为200
* - 页面包含meta refresh标签 */
if (isHttpSuccess && parseMetaRefresh(payload)) {
Serial.println("处理HTML meta refresh重定向");
return fetchAndParseAuthPage(_authUrl);
}
/* 情况3:无认证要求
* - 设置相应的错误信息 */
if (isHttpSuccess) {
_lastError = "正常访问百度首页,无需认证";
} else {
_lastError = "未检测到认证要求,HTTP状态码: " + String(httpCode);
}
return false;
}
bool WiFiAuth::fetchAndParseAuthPage(const String& url) {
WiFiClient client;
HTTPClient http;
if (url.startsWith("https")) {
WiFiClientSecure secureClient;
secureClient.setInsecure();
if (!http.begin(secureClient, url)) {
_lastError = "HTTPS begin failed";
return false;
}
} else if (!http.begin(client, url)) {
_lastError = "HTTP begin failed";
return false;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
_lastError = "获取认证页失败,状态码: " + String(httpCode);
http.end();
return false;
}
String payload = http.getString();
http.end();
if (!_parseAuthPage(payload)) {
_lastError = "解析认证页失败";
return false;
}
return true;
}
bool WiFiAuth::parseMetaRefresh(const String& payload) {
int metaPos = payload.indexOf("http-equiv=\"refresh\"");
if (metaPos == -1) return false;
int urlPos = payload.indexOf("URL=", metaPos);
if (urlPos == -1) return false;
urlPos += 4;
int urlEnd = payload.indexOf("\"", urlPos);
if (urlEnd == -1) urlEnd = payload.indexOf("'", urlPos);
if (urlEnd > urlPos) {
_authUrl = payload.substring(urlPos, urlEnd);
if (_authUrl.startsWith("/")) {
_authUrl = "http://" + getDomainFromUrl("http://www.baidu.com") + _authUrl;
}
return true;
}
return false;
}
String WiFiAuth::getDomainFromUrl(const String& url) {
int start = url.indexOf("://") + 3;
if (start < 3) start = 0;
int end = url.indexOf("/", start);
if (end == -1) end = url.length();
return url.substring(0, end);
}
String WiFiAuth::extractFieldName(const String& html, const String& pattern) {
int pos = html.indexOf(pattern);
if (pos == -1) return "";
pos += pattern.length();
int start = html.indexOf("\"", pos) + 1;
if (start <= 0) start = html.indexOf("'", pos) + 1;
if (start <= 0) return "";
int end = html.indexOf(html[start-1] == '\"' ? "\"" : "'", start);
if (end <= start) return "";
return html.substring(start, end);
}
bool WiFiAuth::_parseAuthPage(const String& html) {
Serial.println("_parseAuthPage called!");
bool isTraditionalForm = parseTraditionalForm(html);
if (!isTraditionalForm) {
parseJSAjaxSubmit(html);
}
return !_formAction.isEmpty() && !_fieldUser.isEmpty() && !_fieldPass.isEmpty();
}
bool WiFiAuth::parseTraditionalForm(const String& html) {
int formStart = html.indexOf("<form");
if (formStart == -1) return false;
int actionStart = html.indexOf("action=\"", formStart);
if (actionStart == -1) actionStart = html.indexOf("action='", formStart);
if (actionStart == -1) return false;
actionStart += (html[actionStart+6] == '\"') ? 8 : 7;
int actionEnd = html.indexOf(html[actionStart-1] == '\"' ? "\"" : "'", actionStart);
if (actionEnd == -1) return false;
_formAction = html.substring(actionStart, actionEnd);
if (_formAction.startsWith("/")) {
_formAction = getDomainFromUrl(_authUrl) + _formAction;
}
_fieldUser = extractFieldName(html, "name=\"username\"");
_fieldPass = extractFieldName(html, "name=\"password\"");
return true;
}
void WiFiAuth::parseJSAjaxSubmit(const String& html) {
String mainJsUrl = findMainJS(html);
Serial.print("mainJsUrl = ");Serial.println(mainJsUrl);
if (mainJsUrl.isEmpty()) return;
String jsContent = fetchJSContent(mainJsUrl);
if (jsContent.isEmpty()) return;
_formAction = parseAjaxUrlFromJS(jsContent);
// 检测数据格式
_dataFormat = detectDataFormat(jsContent);
if (_fieldUser.isEmpty()) {
_fieldUser = parseFieldFromJS(jsContent, "username");
}
if (_fieldPass.isEmpty()) {
_fieldPass = parseFieldFromJS(jsContent, "password");
}
Serial.print("_formAction = ");Serial.println(_formAction);
Serial.print("_fieldUser = ");Serial.println(_fieldUser);
Serial.print("_fieldPass = ");Serial.println(_fieldPass);
Serial.print("_dataFormat = ");Serial.println(_dataFormat == DataFormat::JSON ? "JSON" : "FORM_URLENCODED");
}
WiFiAuth::DataFormat WiFiAuth::detectDataFormat(const String& jsContent) {
// 检查是否有明确的contentType设置
int contentTypePos = jsContent.indexOf("contentType");
if (contentTypePos != -1) {
int jsonPos = jsContent.indexOf("application/json", contentTypePos);
if (jsonPos != -1) {
return DataFormat::JSON;
}
int formPos = jsContent.indexOf("application/x-www-form-urlencoded", contentTypePos);
if (formPos != -1) {
return DataFormat::FORM_URLENCODED;
}
}
// 检查数据是否使用JSON.stringify
if (jsContent.indexOf("JSON.stringify") != -1) {
return DataFormat::JSON;
}
// 检查是否是简单的键值对拼接
if (jsContent.indexOf("&") != -1 && jsContent.indexOf("=") != -1) {
return DataFormat::FORM_URLENCODED;
}
// 检查是否有对象字面量作为数据
if (jsContent.indexOf("data: {") != -1 || jsContent.indexOf("data:{") != -1) {
return DataFormat::JSON;
}
// 默认情况下,如果是$.ajax或$.post,假设是form-urlencoded
if (jsContent.indexOf("$.ajax") != -1 || jsContent.indexOf("$.post") != -1) {
return DataFormat::FORM_URLENCODED;
}
return DataFormat::UNKNOWN;
}
String WiFiAuth::findMainJS(const String& html) {
const char* excludeLibs[] = {"jquery", "zebra", "bootstrap", "vue", "react"};
const char* candidateFiles[] = {"mobile.js", "auth.js", "login.js", "portal.js"};
int scriptPos = 0;
while ((scriptPos = html.indexOf("<script", scriptPos)) != -1) {
int srcPos = html.indexOf("src=", scriptPos);
if (srcPos != -1) {
int urlStart = html.indexOf("\"", srcPos) + 1;
int urlEnd = html.indexOf("\"", urlStart);
String jsUrl = html.substring(urlStart, urlEnd);
bool isLib = false;
for (const char* lib : excludeLibs) {
if (jsUrl.indexOf(lib) != -1) {
isLib = true;
break;
}
}
if (!isLib) {
return resolveRelativeUrl(jsUrl);
}
}
scriptPos += 7;
}
String baseUrl = getBaseUrl(_authUrl);
for (const char* file : candidateFiles) {
String testUrl = baseUrl + "js/" + file;
if (testJsUrlExists(testUrl)) {
return testUrl;
}
}
return "";
}
String WiFiAuth::resolveRelativeUrl(const String& url) {
if (url.startsWith("http")) {
return url;
}
String base = getBaseUrl(_authUrl);
if (url.startsWith("/")) {
return base + url.substring(1);
} else {
return base + url;
}
}
String WiFiAuth::getBaseUrl(const String& fullUrl) {
int protocolEnd = fullUrl.indexOf("://") + 3;
int pathStart = fullUrl.indexOf("/", protocolEnd);
if (pathStart == -1) {
return fullUrl + "/";
}
int lastSlash = fullUrl.lastIndexOf("/");
if (lastSlash <= protocolEnd + 1) {
return fullUrl + "/";
}
return fullUrl.substring(0, lastSlash + 1);
}
bool WiFiAuth::testJsUrlExists(const String& url) {
WiFiClient client;
HTTPClient http;
http.setTimeout(2000);
if (url.startsWith("https")) {
WiFiClientSecure secureClient;
secureClient.setInsecure();
if (!http.begin(secureClient, url)) return false;
} else {
if (!http.begin(client, url)) return false;
}
int httpCode = http.sendRequest("HEAD");
http.end();
return httpCode == HTTP_CODE_OK;
}
String WiFiAuth::fetchJSContent(const String& jsUrl) {
String fullUrl;
if (jsUrl.startsWith("http")) {
fullUrl = jsUrl;
} else if (jsUrl.startsWith("/")) {
fullUrl = getDomainFromUrl(_authUrl) + jsUrl;
} else {
String baseUrl = _authUrl;
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
fullUrl = baseUrl + "/" + jsUrl;
}
HTTPClient http;
http.setTimeout(5000);
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
if (!http.begin(fullUrl)) {
return "";
}
http.addHeader("Accept", "application/javascript, text/javascript, */*");
http.addHeader("User-Agent", "Mozilla/5.0");
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
http.end();
return "";
}
String jsContent = http.getString();
http.end();
if (jsContent.length() < 10 ||
(jsContent.indexOf("function") == -1 &&
jsContent.indexOf("var") == -1 &&
jsContent.indexOf("=") == -1)) {
return "";
}
return jsContent;
}
String WiFiAuth::parseAjaxUrlFromJS(const String& jsContent) {
// 1. 检查标准 $.ajax 调用
int ajaxPos = jsContent.indexOf("$.ajax({");
if(ajaxPos == -1) {
ajaxPos = jsContent.indexOf("$.ajax({");
}
if(ajaxPos != -1) {
// 查找 url 属性
int urlPos = jsContent.indexOf("url", ajaxPos);
if(urlPos != -1) {
int colonPos = jsContent.indexOf(":", urlPos);
int quotePos1 = jsContent.indexOf("\"", colonPos);
int quotePos2 = jsContent.indexOf("'", colonPos);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos != -1) {
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos != -1) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
// 检查是否是登录相关URL
if(url.indexOf("login") != -1 || url.indexOf("auth") != -1 ||
url.indexOf("certify") != -1 || url.indexOf("portal") != -1) {
// 处理相对URL
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
}
// 2. 检查 $.post 或 $.get 调用
int postPos = jsContent.indexOf("$.post(");
if(postPos == -1) {
postPos = jsContent.indexOf("$.get(");
}
if(postPos != -1) {
int parenOpen = jsContent.indexOf("(", postPos);
int quotePos1 = jsContent.indexOf("\"", parenOpen);
int quotePos2 = jsContent.indexOf("'", parenOpen);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos != -1) {
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos != -1) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
if(url.indexOf("login") != -1 || url.indexOf("auth") != -1 ||
url.indexOf("certify") != -1 || url.indexOf("portal") != -1) {
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
// 3. 检查现代 fetch API 调用
int fetchPos = jsContent.indexOf("fetch(");
if(fetchPos != -1) {
int quotePos1 = jsContent.indexOf("\"", fetchPos);
int quotePos2 = jsContent.indexOf("'", fetchPos);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos != -1) {
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos != -1) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
if(url.indexOf("login") != -1 || url.indexOf("auth") != -1 ||
url.indexOf("certify") != -1 || url.indexOf("portal") != -1) {
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
// 4. 检查 axios 调用
int axiosPos = jsContent.indexOf("axios.");
if(axiosPos != -1) {
int methodPos = jsContent.indexOf("post", axiosPos);
if(methodPos == -1) methodPos = jsContent.indexOf("get", axiosPos);
if(methodPos != -1) {
int quotePos1 = jsContent.indexOf("\"", methodPos);
int quotePos2 = jsContent.indexOf("'", methodPos);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos != -1) {
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos != -1) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
if(url.indexOf("login") != -1 || url.indexOf("auth") != -1 ||
url.indexOf("certify") != -1 || url.indexOf("portal") != -1) {
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
}
// 5. 检查直接赋值形式的URL
int urlAssignPos = jsContent.indexOf("url =");
if(urlAssignPos == -1) {
urlAssignPos = jsContent.indexOf("url=");
}
if(urlAssignPos != -1) {
int quotePos1 = jsContent.indexOf("\"", urlAssignPos);
int quotePos2 = jsContent.indexOf("'", urlAssignPos);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos != -1) {
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos != -1) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
if(url.indexOf("login") != -1 || url.indexOf("auth") != -1 ||
url.indexOf("certify") != -1 || url.indexOf("portal") != -1) {
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
// 6. 检查常见的登录API命名模式
const char* commonPatterns[] = {
"/api/login",
"/auth/login",
"/portal/login",
"/user/login",
"/account/login"
};
for(const char* pattern : commonPatterns) {
int pos = jsContent.indexOf(pattern);
if(pos != -1) {
// 尝试提取完整URL
int quotePos1 = jsContent.indexOf("\"", pos);
int quotePos2 = jsContent.indexOf("'", pos);
int startQuotePos = (quotePos1 != -1 && (quotePos2 == -1 || quotePos1 < quotePos2)) ? quotePos1 : quotePos2;
if(startQuotePos < pos) { // 确保引号在模式之前
char quoteChar = jsContent.charAt(startQuotePos);
int endQuotePos = jsContent.indexOf(quoteChar, startQuotePos + 1);
if(endQuotePos > pos) {
String url = jsContent.substring(startQuotePos + 1, endQuotePos);
if(url.startsWith("/")) {
return getDomainFromUrl(_authUrl) + url;
}
return url;
}
}
}
}
return "";
}
String WiFiAuth::parseFieldFromJS(const String& jsContent, const String& fieldType) {
String pattern = "\\." + fieldType + "\\s*=\\s*([^;]+)";
int pos = jsContent.indexOf(pattern);
if (pos != -1) {
int valStart = jsContent.indexOf("\"", pos) + 1;
int valEnd = jsContent.indexOf("\"", valStart);
return jsContent.substring(valStart, valEnd);
}
return "";
}
bool WiFiAuth::_parseAuthPageFromUrl(const String& url) {
WiFiClient client;
HTTPClient http;
if (url.startsWith("https")) {
WiFiClientSecure secureClient;
secureClient.setInsecure();
if (!http.begin(secureClient, url)) return false;
} else if (!http.begin(client, url)) {
return false;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
http.end();
return false;
}
bool result = _parseAuthPage(http.getString());
http.end();
return result;
}
String WiFiAuth::urlEncode(const String& str) {
String encoded;
const char* hex = "0123456789ABCDEF";
for (unsigned int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else if (c == ' ') {
encoded += '+';
} else {
encoded += '%';
encoded += hex[(c >> 4) & 0xF];
encoded += hex[c & 0xF];
}
}
return encoded;
}
bool WiFiAuth::submitAuth(const String& username, const String& password) {
if (_formAction.isEmpty() && !_parseAuthPageFromUrl(_authUrl)) {
_lastError = "No form action available";
return false;
}
WiFiClient client;
HTTPClient http;
if (_formAction.startsWith("https")) {
WiFiClientSecure secureClient;
secureClient.setInsecure();
if (!http.begin(secureClient, _formAction)) {
_lastError = "HTTPS begin failed";
return false;
}
} else if (!http.begin(client, _formAction)) {
_lastError = "HTTP begin failed";
return false;
}
String postData;
if (_dataFormat == DataFormat::JSON) {
http.addHeader("Content-Type", "application/json");
postData = "{\"" + _fieldUser + "\":\"" + username +
"\",\"" + _fieldPass + "\":\"" + password + "\"}";
} else {
// 默认为form-urlencoded
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
postData = _fieldUser + "=" + urlEncode(username) +
"&" + _fieldPass + "=" + urlEncode(password);
}
Serial.print("submitAuth postData = ");Serial.println(postData);
int httpCode = http.POST(postData);
String response = http.getString();
http.end();
// 更全面的认证成功判断逻辑
bool success = false;
if (httpCode == HTTP_CODE_OK ||
httpCode == HTTP_CODE_FOUND ||
httpCode == HTTP_CODE_TEMPORARY_REDIRECT) {
// 检查响应内容中的成功标志
if (_dataFormat == DataFormat::JSON) {
// 解析JSON响应
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, response);
if (!error) {
// 检查常见的JSON响应格式
if (doc.containsKey("success") && doc["success"] == true) {
success = true;
}
else if (doc.containsKey("reply_code") && doc["reply_code"] == 0) {
success = true;
}
else if (doc.containsKey("status") && doc["status"] == "success") {
success = true;
}
}
} else {
// 检查HTML响应中的常见成功标志
if (response.indexOf("认证成功") != -1 ||
response.indexOf("登录成功") != -1 ||
response.indexOf("success") != -1 ||
response.indexOf("欢迎") != -1) {
success = true;
}
}
// 检查重定向URL是否包含成功标志
String location = http.getLocation();
if (!location.isEmpty() &&
(location.indexOf("success") != -1 ||
location.indexOf("welcome") != -1)) {
success = true;
}
}
if (!success) {
// 尝试从响应中提取错误信息
String errorMsg;
if (_dataFormat == DataFormat::JSON) {
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, response);
if (!error) {
if (doc.containsKey("message")) {
errorMsg = doc["message"].as<String>();
} else if (doc.containsKey("error")) {
errorMsg = doc["error"].as<String>();
} else if (doc.containsKey("reply_msg")) {
errorMsg = doc["reply_msg"].as<String>();
}
}
}
if (errorMsg.isEmpty()) {
// 从HTML中提取错误信息
int errorStart = response.indexOf("error-msg");
if (errorStart == -1) errorStart = response.indexOf("errormsg");
if (errorStart != -1) {
int msgStart = response.indexOf(">", errorStart) + 1;
int msgEnd = response.indexOf("<", msgStart);
if (msgEnd > msgStart) {
errorMsg = response.substring(msgStart, msgEnd);
}
}
}
_lastError = String("Auth failed (") + httpCode + ")";
if (!errorMsg.isEmpty()) {
_lastError += ": " + errorMsg;
} else if (!response.isEmpty()) {
// 只显示响应前100个字符,避免内存问题
_lastError += ": " + response.substring(0, 100);
}
}
return success;
}
String WiFiAuth::getLastError() const {
return _lastError;
}
五、使用方法
1.首先你要确保正常连接上WIFI热点
连接热点的部分代码随便找找就能找到,不是本文重点。
连接上WIFI后调后这段代码。
WiFiAuth wifiAuth("http://auth.example.com");
// 检查是否需要认证
if (wifiAuth.checkRedirect()) {
// 此时已自动完成:
// 1. 重定向检测
// 2. 认证页获取
// 3. 表单解析
Serial.println("准备提交认证...");
if (wifiAuth.submitAuth("你的用户名", "你的密码")) {
Serial.println("认证成功!");
} else {
Serial.println("认证失败: " + wifiAuth.getLastError());
}
} else {
Serial.println("无需认证: " + wifiAuth.getLastError());
}
2.一点说明
代码会在WIFI连上热点后,通过checkRedirect()方法去尝试连接www.baidu.com,
如果能正常打开,说明这个网络是不需要认证或者是已经认证过了。有的网络只需要在首次连接时认证,后台会记录设备MAC地址,后续连接就不需要认证了。
如果不能正常打开,代码会检查返回的信息是直接跳转还是Meta refresh,并接着打开跳转的页面,通过抓取HTML表单、分析java-script文件的方式,最终找出登录的API接口。
wifiAuth.submitAuth("你的用户名", "你的密码")方法将你的认证信息填进去,ESP32就会向登录的API接口提交你的用户名和密码,通过返回信息判断是否登录成功。
3.代码还有很多需要完善的地方
代码仅在我目前的网络环境中(认证服务器返回的一个js文件中找到了Ajax的POST json方式提交的登录信息 )测试通过,其他网络环境我没有测试过,可能会存在很多BUG和不完善的地方。
同时如果是其他客户端方式登录(CS),我还没想到怎么解决。
更多推荐



所有评论(0)