Wemos authenticate qua laravel (phần 4.4 - Server kết nối với Wemos)
Bài đăng này đã không được cập nhật trong 6 năm
Giới thiệu
Xin chào các bạn! Bài trước mình đã giới thiệu với các bạn cách tạo gửi request GET và POST trong wemos. Hôm nay chúng ta sẽ cùng xây dựng chức năng đăng nhập vào server laravel và kết nối socket.io sử dụng jwt để xác thực cho wemos nhé.
Xem lại sơ đồ cho đỡ quên nào:
Mình sẽ nói lại các bước hoạt động để các bạn dễ hình dung nhé:
- Gửi request đăng nhập vào server laravel và nhận được jwt-token
- Kết nối với socket.io
- On event thay đổi trạng thái của đèn. Khi có event thì thay đổi trạng thái tương ứng.
- Emit trạng thái hiện tại của đèn
Ở phần Kết nối với socket.io do có bước xác thực người dùng nên luồng kết nối sẽ như sau:
- Thiết lập bắt tay
- Thực hiện authenticate sử dụng jwt-token đã nhận được
- Chuyển đổi giao thức từ HTTP thành Web socket
Viết chương trình server
Chúng ta sẽ sử dụng chương trình server đã tạo từ bài trước.
Các bạn hãy cài đặt như project laravel bình thường sau đó chạy lệnh php artisan migrate --seed
để tạo dữ liệu test nhé.
Sửa lại device models
Muốn thư viện tymon/jwt-auth
có thể hoạt động với Device
model ta cần phải implements JWTSubject cho Device
model. Và phải thêm 2 hàm phục vụ chức năng lấy các trường để tạo auth-token.
// app\Models\Device.php
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
// Khi nodejs nhận được chuỗi jwt và parse ra sẽ được dữ liệu trả về trong hàm này
return [
'id' => $this->id,
'identify_code' => $this->identify_code,
'guard' => 'device',
];
}
Tạo route
Route đăng nhập, đăng xuất sẽ như sau:
Route::group(['prefix' => 'device', 'namespace' => 'Device'], function () {
Route::get('session', 'SessionController@session');
Route::post('login', 'SessionController@login');
Route::post('logout', 'SessionController@logout');
});
Tạo controller đăng nhập
Tạo controller App\Http\Controllers\Device\SessionController
.
Ta cần phải tạo 3 hàm tương ứng với 3 chức năng:
- Kiểm tra trạng thái đăng nhập (Hàm session)
- Đăng nhập (Hàm login)
- Đăng xuất (Hàm logout)
Dữ liệu trả về có các status sau:
- login_in: Đã đăng nhập thành công vào hệ thống
- not_login: Chưa đăng nhập
- login_fail: Đăng nhập thất bại
- logout_success: Đăng xuất thành công
const RESPON_STATUS = [
'not_login' => 0,
'login_in' => 1,
'login_fail' => 2,
'logout_success' => 3,
];
Hàm get session
Trả về dữ liệu đã login thành công hoặc là chưa login
public function session(JWTAuth $auth)
{
if (Auth::guard($this->guard)->check()) {
return $this->userWasAuthenticated($auth);
}
return $this->sessionResponse(self::RESPON_STATUS['not_login']);
}
Dữ liệu trả về khi đã login
protected function userWasAuthenticated($auth)
{
$user = Auth::guard($this->guard)->user();
if ($user->isActive()) {
// Lấy auth token từ user đã đăng nhập
$authToken = $auth->fromUser($user);
$result = $this->makeUserResult($user, $authToken);
return $this->sessionResponse(self::RESPON_STATUS['login_in'], $result);
}
$this->logout();
return $this->sessionResponse(self::RESPON_STATUS['login_fail']);
}
Hàm login
Giúp device đăng nhập vào hệ thống
public function login(Request $request, JWTAuth $auth)
{
$this->validateLogin($request);
$credentials = $request->only('identify_code', 'password');
if ($this->attemptLogin($request, $credentials)) {
return $this->userWasAuthenticated($auth);
}
return $this->sessionResponse(self::RESPON_STATUS['login_fail']);
}
Hàm logout
Hàm đăng xuất
public function logout()
{
Auth::guard($this->guard)->logout();
return $this->sessionResponse(self::RESPON_STATUS['logout_success']);
}
Hàm sessionResponse
Trả về dữ liệu với token để Device có thể sử dụng cho việc gửi post request.
protected function sessionResponse($status, $data = [], $statusCode = 200)
{
$data['status'] = $status;
$data['token'] = csrf_token();
return response()->json($data, $statusCode);
}
Phần này khá đơn giản. Các bạn có thể đọc code full tại đây.
Bây giờ chỉ còn việc viết chương trình cho wemos thôi. Bắt đầu thực hiện từng bước một nào!
Chúng ta sẽ sử dụng thư viện esp8266-socket.io này để kết nối socket.
Các bạn tải hoặc clone thư viện này về xong coppy vào thư mục /home/{user}/Arduino/libraries
là có thể sử dụng được rồi.
Gửi request đăng nhập vào server laravel
Viết hàm gửi request và lưu cookies lại
Đầu tiên chúng ta cần phải biết một điều là Laravel
sẽ giữ session đăng nhập bằng cookies
. Nếu không lưu lại thì request sau bạn gửi sẽ thành session khác. Và tất nhiên khi mà bạn đăng nhập ở request trước rồi, request sau server vẫn coi là chưa đăng nhập nếu bạn không gửi cookies kèm theo.
Chúng ta vẫn sử dụng các hàm waitForInput
và readLine
của bài trước nhé.
// Hàm gửi request
void sendRequest(String method, String host, int port, String path, String data) {
char hostname[128];
host.toCharArray(hostname, MAX_HOSTNAME_LEN);
// Kết nối đến host và post
if (!internets.connect(hostname, port)) {
ECHO(F("Connect failed"));
return;
}
// Tạo request
String request = "";
request += method;
request += F(" /");
request += path;
request += F(" HTTP/1.1\r\n");
request += F("Host: ");
request += hostname;
request += F("\r\n");
request += F("Accept: application/json\r\n");
// Thêm token vào header mặc định khởi tạo là rỗng. Sau khi gửi get session server trả về token thì mình sẽ lưu lại.
request += F("X-CSRF-TOKEN: ");
request += token; // Biến toàn cục
request += F("\r\n");
request += F("Cookie: ");
request += cookie; // cookie cũng sẽ được thêm khi parse response nhận được. Ban đầu là rỗng
request += F("\r\n");
request += ORIGIN;
// Nếu có dữ liệu body thì phải thêm trường Content-Length vào header
if (data.length() > 0) {
request += F("Content-Length: ");
request += data.length();
request += F("\r\n");
}
request += F("Content-Type: application/json\r\n");
request += F("Connection: keep-alive\r\n\r\n");
// Nếu có dữ liệu body thì thêm body vào
if (data.length() > 0) {
request += data;
request += F("\r\n");
}
ECHO(F("\r\n[sendRequest] Send request........................."));
ECHO(request);
ECHO(F("[sendRequest] .........................send request done\r\n"));
// Gửi request đi
internets.print(request);
// Chờ response trả về
if (!waitForInput()) {
ECHO(F("[sendRequest] Time out"));
}
}
// Thêm cookie mới vào chuỗi cookie cũ
void setCookie(String data) {
int a = data.indexOf(";");
String newCookie = data.substring(12, a);
String newCookieKey = newCookie.substring(0, newCookie.indexOf("=")); // Lấy giá trị của key
int oldCookieStart = cookie.indexOf(newCookieKey); // Kiểm tra xem key đã tồn tại không
if (oldCookieStart >= 0) {
// Nếu cookie mới đã tồn tại thì thay thế cookie cũ bằng cookie mới
int oldCookieEnd = cookie.indexOf(";", oldCookieStart);
String newCookies = cookie.substring(0, oldCookieStart);
newCookies += newCookie;
newCookies += cookie.substring(oldCookieEnd);
cookie = newCookies;
} else {
// Nếu cookie mới chưa tồn tại thì nối vào chuỗi cookie cũ
cookie += newCookie;
cookie += ";";
}
ECHO("cookie");
ECHO(cookie);
}
Dữ liệu gửi về sẽ ở dạng stream các byte. Bạn có thể hiểu là nó sẽ truyền từng byte một toàn bộ response, từ header đến body. Và wemos sẽ lưu vào bộ đệm. Mình có thể đọc lần lượt từng byte. Vì mình không biết có bao nhiêu byte được lưu trữ nên không thể đọc toàn bộ response trả về. Và cũng không nên làm như vậy vì rất tốn bộ nhớ. Mình phải đọc hết phần header thì mới có thể đọc đến body. Vì phần header không dùng nên mình sẽ viết hàm xóa header đi.
void eatHeader() {
while (internets.available()) {
// Đọc từng dòng
readLine();
// Nếu thấy 1 dòng trống, tức là đã hết dữ liệu header thì thoát ra.
if (strlen(databuffer) == 0) {
break;
}
String data = databuffer;
// Nếu thấy key là Set-Cookie thì lưu cookie lại
if (data.indexOf("Set-Cookie:") >=0) {
setCookie(data);
}
}
}
Viết hàm stopConnect
Đây là 1 hàm rất quan trọng. Khi bạn gửi http request và xử lý xong thì nhớ gọi hàm stopConnect nhé. Hàm này sẽ xóa bộ đệm và ngắt kết nối với server.
stopConnect() {
while (internets.available()) {
readLine();
}
internets.stop();
delay(100);
ECHO(F("[stopConnect] Connect was stopped"));
return true;
}
Viết hàm getSession
Hàm này có tác dụng kiểm tra xem Device đã đăng nhập hay chưa.
void getSession() {
String path = "device/session";
// Gửi request GET đến path device/session
sendRequest(String("GET"), host, port, path, String(""));
// Loại bỏ header
eatHeader();
// Đọc body
String data = "";
while (internets.available()) {
readLine();
data += String(databuffer);
}
JsonObject& root = jsonBuffer.parse(data);
if (!root.success()) {
Serial.println("Parse json failed");
return;
}
// Lưu giá trị loginStatus
int newStatus = root["status"];
loginStatus = newStatus; // loginStatus là biến toàn cục
// Lưu token lại
String newToken = root["token"];
token = newToken; // token là biến toàn cục
if (loginStatus == 1) {
Serial.println(F("User is login"));
} else {
Serial.println(F("User is not login"));
}
}
Chạy thử hàm này xem có hoạt động đúng không nào:
Dữ liệu trả về hoàn toàn đúng như mong đợi.
Viết hàm login
Ta thêm 2 biến toàn cục lưu identify_code
và password
String identifyCode = "be-ca-1";
String password = "12344321";
Tài khoản device này đã có trong sedder
của server laravel. Nếu bạn chưa chạy seeder thì hãy chạy lệnh php artisan db:seed
để tạo database test nhé.
Hàm login sẽ được viết như sau:
void login() {
StaticJsonBuffer<DATA_BUFFER_LEN> jsonBuffer;
String path = "device/login";
String data = F("{\"identify_code\":\"");
data += identifyCode;
data += "\",\"password\":\"";
data += devicePassword;
data += "\"}";
Serial.print(F("Data login: "));
Serial.println(data);
sendRequest(String("POST"), host, port, path, data);
eatHeader();
data = "";
while (internets.available()) {
readLine();
data += String(databuffer);
}
JsonObject& root = jsonBuffer.parse(data);
if (!root.success()) {
Serial.println("Parse json failed");
return;
}
int status = root["status"];
String newAuthToken = root["auth_token"];
authToken = newAuthToken;
stopConnect();
}
Login thử xem thế nào nào.
Sau khi login xong getSession xem thế nào.
Kết quả như mong đợi. Dòng chữ "User is login" đã hiện ra.
Kết nối với socket.io
Bạn có thể tham khảo phần 3 của series này.
Phần này chỉ khác một cái là ta phải gửi chuỗi authenticate sau khi bắt tay xong. Và trước khi tạo kết nối socket.
Tạo thêm hàm authenticate
vào thư viện.
Ở file SocketIOClient.h
bạn thêm khai báo hàm:
// SocketIOClient.h
private:
/* ..... */
bool authenticate(String authToken);
void sendRequestAuthenticate(String authToken);
Viết hàm authenticate và sendRequestAuthenticate
// SocketIOClient.cpp
bool SocketIOClient::authenticate(String authToken) {
ECHO(authToken);
if (authToken.length() == 0) {
return true;
}
if (!beginConnect()) {
return false;
}
sendRequestAuthenticate(authToken);
if (!waitForInput()) {
ECHO(F("[sendRequestAuthenticate] Time out"));
return false;
}
if (!checkResponseStatus(200)) {
ECHO(F("[sendRequestAuthenticate] Authenticate fail"));
return false;
}
stopConnect();
}
void SocketIOClient::sendRequestAuthenticate(String authToken) {
ECHO("[sendRequestAuthenticate] Authenticate with token: " + authToken);
String body = String(authToken.length() + 31);
body += ":42[\"authenticate\",{\"token\":\"";
body += authToken;
body += "\"}]";
String request = "";
request += F("POST /socket.io/?EIO=3&transport=polling&sid=");
request += sid;
request += F(" HTTP/1.1\r\n");
if (port == 80) {
request += F("Host: ");
request += hostname;
request += F("\r\n");
} else {
request += F("Host: ");
request += hostname;
request += F(":");
request += port;
request += F("\r\n");
}
request += ORIGIN;
request += F("Content-Type: text/plain;charset=UTF-8\r\n");
request += F("Content-Length: ");
request += body.length();
request += F("\r\n");
request += F("Connexion: keep-alive\r\n\r\n");
request += body;
request += "\r\n\r\n";
ECHO(F("\r\n[sendRequestAuthenticate] Send request........................."));
ECHO(request);
ECHO(F("[sendRequestAuthenticate] .........................send request done\r\n"));
internets.print(request);
}
Tại hàm connect của thư viện ta thêm hàm authenticate vào như sau:
// SocketIOClient.cpp
bool SocketIOClient::connect(String thehostname, int theport, String authToken) {
thehostname.toCharArray(hostname, MAX_HOSTNAME_LEN);
port = theport;
ECHO(F("[connect] Connect to host: "));
ECHO(hostname);
ECHO(F("[connect] Connect to port: "));
ECHO(port);
if (handshake() && authenticate(authToken) && connectViaSocket()) {
return true;
}
return false;
}
Nhớ sửa lại phần khai báo hàm trong file SocketIOClient.h cho đúng nhé
// SocketIOClient.h
public:
bool connect(String thehostname, int port = 80, String authToken = "");
Khởi tạo biến toàn cục socket
SocketIOClient socket;
Thêm chức năng kết nối socket
Bạn tham khảo phần này nhé. Mình sẽ không trình bày lại nữa. Bạn cần chú ý ở dòng socket.connect(host, port);
ta thay thành socket.connect(host, port, authToken);
với authToken là cái authToken chúng ta đã vất vả lấy được ở trên.
Các bạn có thể tham khảo code của mình tại đây.
Kết luận
Trên đây mình cùng các bạn đã xây dựng xong chương trình kết nối server cho wemos. Như vậy chúng ta đã đi được gần hết chặng đường làm bể cá thông minh rồi. Và chương trình của mình mỗi khi thay đổi lại phải kết nối với máy tính rồi nạp lại code. Thật phiền phức! Điều tất yếu phải xuất hiện 1 cách nào đó để việc update phần mềm được dễ dàng hơn. Phần tiếp theo mình sẽ chia sẻ cách update firmware qua mạng cho wemos. Các bạn hãy đón đọc nhé!
All rights reserved