Xây dựng Web Server HTTP Authentication với ESP32

Bài viết này hướng dẫn bạn xây dựng một web server HTTP Authentication cho ESP32 bằng Arduino IDE. Đây là Web Server có yêu cầu đăng nhập bằng username và mật khẩu khi người dùng truy cập thông qua HTTP authentication. Người dùng chỉ có thể truy cập vào được trang web nếu nhập đúng username và password.

Chúng ta sẽ vẫn sử dụng thư viện ESPAsyncWebServer như đã giới thiệu ở bài về Web server trước đó.

Để làm theo bài hướng dẫn này, bạn cần cài đặt Arduino IDE và addon cho ESP32 để có thể lập trình cho board ESP32 được. Tham khảo bài viết trước đó:

Mục đích

Hướng dẫn này giúp bạn bảo mật được web server mà bạn dựng lên trong mạng local của mình theo một cách đơn giản, khiến cho những người không được bạn cho phép truy cập sẽ không thể vào được. Tuy nhiên, mức độ bảo mật của phương pháp này là rất cơ bản, không thể áp dụng cho các hệ thống công nghiệp hay các ứng dụng cho phép người dùng từ bên ngoài Internet truy cập vào.

Tổng quan cách thực hiện

Dưới đây là tóm tắt các bước thực hiện của cách làm mà mình sẽ hướng dẫn sau đây.

Tổng quan về dự án Web Server HTTP trên ESP32
Tổng quan về dự án Web Server HTTP trên ESP32
  • Trước tiên bạn sẽ bật chế độ bảo mật bằng username và password cho trang web
  • Khi người dùng truy cập vào bằng địa chỉ IP của ESP32, một cửa sổ popup sẽ hiện ra và yêu cầu người dùng nhập username và password
  • Để truy cập vào, người dùng cần nhập đúng username và password đã cấu hình trong chương trình của ESP32
  • Trong trang web cũng có một nút nhấn Logout để người dùng đăng xuất khỏi trang web
  • Sau khi đăng xuất, người dùng cần nhập lại username và password khi muốn truy cập lại hoặc truy cập từ một thiết bị khác trong cùng mạng
  • Lưu ý là các thông tin đăng nhập này không được bảo mật trong quá trình trao đổi thông tin giữa máy tính dùng để đăng nhập và web server ở đây là ESP32

Lưu ý: Hướng dẫn này được test và chạy tốt trên trình duyệt Google Chrome và Firefox và các thiết bị Android.

Cài đặt thư viện Async Web Server

Để dựng được một web server cho ESP32 bạn cần cài đặt các thư viện sau:

Các thư viện này không có sẵn trong Arduino Library Manager, bạn sẽ cần download file zip về và cài đặt thông qua menu Sketch Include Library > Add .zip Library trong Arduino IDE.

Code của Web Server HTTP ESP32

Bạn hãy copy và paste đoạn code sau vào trong Arduino IDE.

// Import required libraries
#ifdef ESP32
  #include <WiFi.h>
  #include <AsyncTCP.h>
#else
  #include <ESP8266WiFi.h>
  #include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

const char* http_username = "admin";
const char* http_password = "admin";

const char* PARAM_INPUT_1 = "state";

const int output = 2;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 2.6rem;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 10px;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px} 
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
    input:checked+.slider {background-color: #2196F3}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
  </style>
</head>
<body>
  <h2>ESP Web Server</h2>
  <button onclick="logoutButton()">Logout</button>
  <p>Ouput - GPIO 2 - State <span id="state">%STATE%</span></p>
  %BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ 
    xhr.open("GET", "/update?state=1", true); 
    document.getElementById("state").innerHTML = "ON";  
  }
  else { 
    xhr.open("GET", "/update?state=0", true); 
    document.getElementById("state").innerHTML = "OFF";      
  }
  xhr.send();
}
function logoutButton() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/logout", true);
  xhr.send();
  setTimeout(function(){ window.open("/logged-out","_self"); }, 1000);
}
</script>
</body>
</html>
)rawliteral";

const char logout_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <p>Logged out or <a href="/">return to homepage</a>.</p>
  <p><strong>Note:</strong> close all web browser tabs to complete the logout process.</p>
</body>
</html>
)rawliteral";

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons ="";
    String outputStateValue = outputState();
    buttons+= "<p><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></p>";
    return buttons;
  }
  if (var == "STATE"){
    if(digitalRead(output)){
      return "ON";
    }
    else {
      return "OFF";
    }
  }
  return String();
}

String outputState(){
  if(digitalRead(output)){
    return "checked";
  }
  else {
    return "";
  }
  return "";
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(output, OUTPUT);
  digitalWrite(output, LOW);
  
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    request->send_P(200, "text/html", index_html, processor);
  });
    
  server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(401);
  });

  server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", logout_html, processor);
  });

  // Send a GET request to <ESP_IP>/update?state=<inputMessage>
  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    if(!request->authenticate(http_username, http_password))
      return request->requestAuthentication();
    String inputMessage;
    String inputParam;
    // GET input1 value on <ESP_IP>/update?state=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = PARAM_INPUT_1;
      digitalWrite(output, inputMessage.toInt());
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    request->send(200, "text/plain", "OK");
  });
  
  // Start server
  server.begin();
}
  
void loop() {
  
}

Trong chương trình bạn sẽ cần thay đổi thông tin WiFi và mật khẩu WiFi của bạn cho đúng.

Trong chương trình trên, chúng ta xây dựng một web server cho phép người dùng điều khiển đèn led ở chân GPIO 2. Bạn có thể bật chế độ bảo mật HTTP authentication một cách dễ dàng với thư viện ESPAsyncWebServer.

Giải thích chương trình

Đoạn code để chạy web server lên đã được giải thích khá chi tiết trong các bài viết về web server khác, nên trong bài viết này, mình chỉ tập trung giải thích đoạn code liên quan đến việc bật chế độ bảo mật HTTP authentication lên mà thôi.

Cài đặt Username và Password

Trong chương trình, chúng ta dùng 2 biến sau để lưu username và password cho web server của chúng ta. Mặc định, username là admin và password cũng là admin. Bạn có thể thay đổi theo ý mình và nên thay đổi để bảo mật tốt hơn.

const char* http_username = "admin";
const char* http_password = "admin";

Nút nhấn Logout

Trong nội dung HTML của trang web lưu trong biến index_html, có một nút nhấn được thêm vào để cho người dùng đăng xuất sau khi làm việc xong.

<button onclick="logoutButton()">Logout</button>

Khi nút này được nhấn, hàm logoutButton() trong JavaScript được gọi. Hàm này gửi HTTP GET request đến ESP32/ESP8266 với đường dẫn là /logout. Trong code của ESP32, chúng ta sẽ xử lý yêu cầu này.

function logoutButton() {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "logout", true);
  xhr.send();

Và một giây sau khi nhấn nút logout, người dùng sẽ được hướng tới trang web có đường dẫn là /logged-out.

  setTimeout(function(){ window.open("/logged-out","_self"); }, 1000);
}

Xử lý request từ web trong ESP32

Mỗi khi người dùng thao tác trên web, một yêu cầu sẽ được gửi về ESP32, Trong chương trình của ESP32, chúng ta sẽ kiểm tra xem người dùng đã đăng nhập hay chưa.

if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();

Nế người dùng chưa đăng nhập thì trang web sẽ yêu cầu đăng nhập. Bạn cần kiểm tra và yêu cầu đăng nhập tương tự cho tất cả các trang của web server. Nếu người dùng nhập sai username và password thì trang web sẽ mở ra popup lại lần nữa và yêu cầu nhập lại.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  request->send_P(200, "text/html", index_html, processor);
});

Trường hợp người dùng đã đăng nhập, ESP32 sẽ xử lý bình thường và trả về kết quả với mã 200 OK.

server.on("/state", HTTP_GET, [] (AsyncWebServerRequest *request) {
  if(!request->authenticate(http_username, http_password))
    return request->requestAuthentication();
  request->send(200, "text/plain", String(digitalRead(output)).c_str());
});

Xử lý yêu cầu Logout

Khi người dùng nhấn nút logout, ESP32 sẽ nhận được yêu cầu với đường dẫn là /logout. Khi đó, ESP32 sẽ gửi response code 401.

server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(401);
});

Response code 401 là một mã lỗi của HTTP báo hiệu yêu cầu không thể thực hiện do chưa đăng nhập, tương tự với việc logout người dùng. Sau 1 giây thì trang web sẽ tự động điều hướng người dùng tới URL /logged-out.

server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", logout_html, processor);
});

Chạy chương trình

Sau khi upload code vào mạch ESP32, bạn mở cửa sổ Serial Monitor và xem địa chỉ IP của board sau khi kết nối WiFi thành công.

Mở trình duyệt và truy cập vào IP này.

Trang web sẽ mở ra popup và yêu cầu nhập username và password. Bạn hãy nhập admin/admin để đăng nhập.

Chạy chương trình Web Server HTTP
Chạy chương trình Web Server HTTP

Sau khi đăng nhập, bạn sẽ vào được trang web.

Vào Web Server ESP32

Bạn có thể nhấn nút để bật tắt đèn LED trên board.

Bật LED trên ESP32
Bật LED trên ESP32

Sau khi điều khiển xong, bạn nhấn vào nút logout để đăng xuất.

Đăng xuất Web Server HTTP

Nếu bạn click vào “return to homepage”, bạn sẽ được điều hướng đến trang chủ.

Đối với Google Chrome, bạn sẽ được yêu cầu nhập username và password để truy cập vào lại.

Đối với Firefox, bạn cần đóng tab và mở tab mới thì mới thấy yêu cầu đăng nhập lại. Nếu không, bạn sẽ vẫn truy cập được mà không bị hỏi username và password.

Nếu bạn truy cập trang Web bằng một thiết bị khác trong cùng mạng, thì bạn sẽ được ye6uc ầu đăng nhập lại.

Truy cập vào Web Server HTTP ESP32
Truy cập vào Web Server HTTP ESP32

Lời kết

Như vậy, bài viết này đã hướng dẫn bạn một cách chi tiết để có thể xây dựng một web server HTTP cho phép điều khiển đèn LED, nhưng bắt buộc người dùng phải đăng nhập. Đây là một giải pháp tăng tính bảo mật nhưng cách làm khá đơn giản và hiệu quả cho các ứng dụng không đòi hỏi cao về tính bảo mật.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *