+22

Xác thực ứng dụng chat Realtime với Laravel Passport, Laravel Echo

Cập nhật gần nhất: 28/11/2024

Hello các bạn lại là mình đây 👋👋

Dạo này dịch bệnh ở VN căng thẳng quá chờ mãi không biết bao giờ con mới được về với đất mẹ 😂😂 Nhớ giữ gìn sức khoẻ nhé các bạn để có sức còn code

Ở các bài trước trong series viết ứng dụng chat này mình dùng session để xác thực account của user cùng với đó là xác thực cho phía laravel echo. Thế nhưng ở các dự án thật thì việc xác thực qua JWT Token cũng rất phổ biến. Có khá nhiều bạn đã hỏi mình về vấn đề này.

Hôm nay tranh thủ rảnh mình viết luôn bài này để từ nay về sau các bạn có cái để xem trực tiếp và làm theo luôn chứ không cần phải lọ mọ search google nữa 😘

Bài sẽ khá là ngắn chứ không dài dòng văn tự như các bài khác nên các bạn yên tâm nhé 😂😂

Setup

Các bạn clone source code ở đây: https://github.com/maitrungduc1410/viblo-private-chat-app

Sau khi clone các bạn chạy lần lượt command sau để setup project:

composer install
npm install
cp .env.example .env
php artisan key:generate

Sau đó các bạn setup lại thông số database ở file .env cho phù hợp với máy của các bạn nha, ví dụ bên dưới của mình nom như sau:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=chat_app
DB_USERNAME=root
DB_PASSWORD=

Ta có thể tạo sẵn database chat_app (hoặc không thì tí nữa đoạn migrate Laravel làm thay cũng được) 😁

Oke thì ta migrate và seed dữ liệu nhé:

php artisan migrate --seed

Screenshot 2024-11-24 at 10.41.07 PM.jpg

Cuối cùng chạy app để test thử coi xem sao nhen, các bạn chạy các command sau:

php artisan serve
npm run dev

Sau đó ta mở trình duyệt ở địa chỉ http://localhost:8000/, tiến hành tạo tài khoản login, zô gửi vào tin nhắn các kiểu đảm bảo mọi thứ okela nha 😎:

Screenshot 2024-11-24 at 10.42.41 PM.jpg

Screenshot 2024-11-24 at 10.42.53 PM.jpg

Screenshot 2024-11-24 at 10.43.00 PM.jpg

Cài Passport

Ở bài này thì thay vì dùng cách xác thực thông thường thì ta sẽ dùng Laravel Passport nhé: https://laravel.com/docs/master/passport

Đầu tiên là ta cài Passport:

php artisan install:api --passport

Khi được hỏi Would you like to use UUIDs for all client IDs? (yes/no) thì ta cứ mặc định chọn No nha

Tiếp đó ta mở file app/Models/User.php và thêm trait Laravel\Passport\HasApiTokens vào:

20241125-222456.jpg

Sau đó ta mở file config/auth.php và thêm 1 auth guard cho api

20241125-222645.jpg

Âu cây roài, ta tới phần realtime nha 💪💪

Realtime

Setup broadcast

Âu cây, hiện tại ở Laravel 11 thì phần Broadcasting đã không được cài sẵn mà ta phải tự cài nó với command sau:

php artisan install:broadcasting

Khi được hỏi có cài Reverb + Node dependencies không thì ta chọn Yes nhé:

Screenshot 2024-11-24 at 5.03.26 PM.jpg

Screenshot 2024-11-24 at 5.03.45 PM.jpg

Khi cài Oke thì ở .env ta phải thấy xêm xêm như sau:

Screenshot 2024-11-24 at 5.05.45 PM.jpg

Chú ý: BROADCAST_CONNECTION=reverb nhé

Tiếp theo ta cài thêm 2 packages sau cho Frontend nhé:

npm install --save-dev laravel-echo pusher-js

Lý do ở trên ta cài pusher-js là vì Reverb nó cũng follow theo protocol của Pusher

Tiếp theo ta mở file resources/js/bootstrap.js, kéo xuống dưới cùng sẽ thấy là ở đoạn install broadcasting vừa nãy nó đã tự động thêm:

import './echo';

Và file echo.js có nội dung như sau cũng đã được tạo ra:

import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

Vì mặc định thì Laravel Echo sẽ gọi tới broadcasting/auth để xác thực channel, giờ ta dùng API route thì ta cần phải update lại, các bạn sửa lại toàn bộ file như sau:

import Echo from "laravel-echo";
import Pusher from "pusher-js";
import axios from "axios";

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: "reverb",
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
    authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios
                    .post(
                        "/api/broadcasting/auth",
                        {
                            socket_id: socketId,
                            channel_name: channel.name,
                        },
                        {
                            headers: {
                                Authorization: `Bearer ${localStorage.getItem(
                                    "token"
                                )}`,
                            },
                        }
                    )
                    .then((response) => {
                        console.log(1111, response.data);
                        callback(false, response.data);
                    })
                    .catch((error) => {
                        callback(true, error);
                    });
            },
        };
    },
});

Ở trên các bạn thấy mình thêm vào authorizer, logic khá đơn giản, ta gọi API tới /api/broadcasting/auth, lấy token được lưu ở local storage và gửi đi để xác thực với Laravel API route, kết quả trả về ta đưa vào callback, phần còn lại thì Laravel Echo lo hết 😎

Tiếp theo, ta tạo event MessagePosted (event này sẽ được gọi mỗi khi có tin nhắn được lưu thành công):

php artisan make:event MessagePosted

Sau đó ta mở file app/Events/MessagePosted và sửa lại như sau:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

use App\Models\Message;

class MessagePosted implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    /**
     * Create a new event instance.
     */
    public function __construct(Message $message)
    {
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn()
    {
        return new PrivateChannel('room.'.$this->message->room_id);
    }
}

Chú ý đoạn implements ShouldBroadcastuse App\Models\Message; nha 😉

Giải thích chút nhé: ở event này ta nhận vào 1 tham số là message vừa được lưu, sau đó broadcast cho các users khác qua Private channel với tên channel là room của message.

Tiếp theo, vì là bây giờ ta dùng Private Channel, nên ta phải login trước thì lát nữa mới có thể lắng nghe sự kiện nhé. Đồng thời, khi login xong user có tuỳ quyền chọn vào 1 trong các chatroom định sẵn do đó ở file routes/channels.php ta thêm vào như sau nhé:

<?php

use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('room.{id}', function ($user, $id) {
    return true; // user có thể join vào bất kì chatroom nào
});

Chú ý rằng ở callback ta có $user là user sau khi đã authenticated, tức là đã đảm bảo rằng họ đã login

Setup phần broadcast ổn rồi đó. Giờ ta quay lại file MessageController.php ở hàm store (để lưu tin nhắn) ta sửa lại như sau:

use App\Events\MessagePosted; // thêm dòng này ở đầu

...

public function store (Request $request) {
    $message = new Message();
    $message->room_id = $request->input('room_id', -1);
    $message->user_id = Auth::user()->id;
    $message->content = $request->input('content', '');

    $message->save();

    // Thêm dòng bên dưới
    // Gửi đến các user khác trong phòng TRỪ user tạo tin nhắn này
    broadcast(new MessagePosted($message->load('user')))->toOthers();

    return response()->json(['message' => $message->load('user')]);
}

Ổn rồi đó, giờ ta qua frontend để lắng nghe sự kiện bắt tin nhắn mới nhé 😘

Các bạn mở file resources/js/pages/Room.vueonBeforeMount các bạn sửa lại như sau:

onBeforeMount(() => {
  getMessages()
  const index = rootData.rooms.findIndex(item => item.id === parseInt(route.params.roomId))
  if (index > -1) {
    currentRoom.value = rootData.rooms[index]

    Echo.private(`room.${currentRoom.value.id}`)
      .listen('MessagePosted', (e) => {
        messages.value.push(e.message)

        scrollToBottom()
      })
  }
})

Giải thích chút nhé:

  • onBeforeMount đầu tiên ta lấy danh sách tin nhắn ứng với room ID hiện tại, room ID được trichs từ URL hiên tại (dùng route.params.roomId)
  • vẫn ở onBeforeMount ta check xem room ID có tồn tại trong danh sách room hay không (đề phòng user tự sửa room id và cho load lại trang)
  • nếu có thì ta mới tiến hành lắng nghe tin nhắn dùng Echo.private, khi có tin nhắn mới thì ta đẩy vào mảng danh sách message

Tiêp theo đó vẫn ở Room.vue ta thêm vào:

import { onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount } from 'vue'

//...

onBeforeUnmount(() => {
  // huỷ lắng nghe tin nhắn ở chatroom hiện tại
  // nếu như user chuyển qua route/chatroom khác
  Echo.leaveChannel(`room.${currentRoom.value.id}`)
})

Ô kê đến giờ test rồi. Các bạn tắt hết các cửa sổ terminal đang chạy, clear config bằng command php artisan config:clear và ta tiến hành khởi động lại bằng các command sau nhé, mỗi command chạy ở 1 terminal nhé:

php artisan serve
npm run dev
php artisan reverb:start --debug
php artisan queue:work

ta chạy Reverb với option --debug để xem đầy đủ log ở terminal nha

Giờ ta quay trở lại trình duyệt, mở 2 tab, 1 tab thường, 1 ẩn danh, login với 2 account, thử chat loạn xạ và......BÙM, chả có gì xảy ra cả 🥲🥲, mở console trình duyệt thấy như sau:

Screenshot 2024-11-25 at 10.51.11 PM.jpg

Hí, quên, đã viết Route để xác thực cho Laravel Echo đâu 😂😂

Ta mở routes/api.php, ta viết thêm 1 route như sau:

Broadcast::routes(['middleware' => ['auth:api']]);

Ở đây ta đã register - đăng kí tất cả các route liên quan tới Broadcast ở API route và dùng auth:api (Laravel Passport), khi frontend gọi tới thì Passport sẽ xác thực trước, oke thì sẽ trả về response theo những gì được định nghĩa bởi module Broadcast

Xong thì ta quay trở lại trình duyệt F5, sẽ thấy console in ra lỗi 401:

Screenshot 2024-11-25 at 10.55.32 PM.jpg

Lý do là vì ta chưa có lưu token nào ở localstorage để đem ra mà xác thực cả 😂

Xác thực Laravel Echo

Ở bài này, cho đơn giản thì mình sẽ dùng Personal Access Token nhé: https://laravel.com/docs/master/passport#personal-access-tokens

Đầu tiên ta chạy command sau để tạo passport client:

php artisan passport:client --personal

Ta nhập vào tên client muốn tạo:

Screenshot 2024-11-25 at 11.51.31 PM.jpg

Ta sẽ thấy có giá trị client ID và secret, ta copy và tạo 2 biến sau ở .env:

PASSPORT_PERSONAL_ACCESS_CLIENT_ID="3"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="S2iKVwnInd0wERAcusUIUnC0dq3waxu7yxi60G6C"

Ở trên là cái của mình, các bạn nhớ thay giá trị của các bạn vào cho đúng nhé ☺️

Tiếp đó ta mở app/Http/Controllers/AppController.php sửa lại chút như sau để ta trả luôn về token mỗi khi user load app:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Chatroom;
use Auth;

class AppController extends Controller
{
    public function index () {
        $user = Auth::user();
        $token = $user->createToken('My Token')->accessToken;
        $data = ['user' => $user, 'rooms' => Chatroom::all(), 'token' => $token];
        return view('app', ['data' => $data]);
    }
}

Ở trên ta có đoạn $token = $user->createToken('My Token')->accessToken; để tạo access token và trả về cho view

Giờ ta đóng tất cả terminal đi và start mới:

php artisan serve

npm run dev

php artisan reverb:start --debug

php artisan queue:work
  • reverb: là websocket server
  • queue:work: vì event MessagePosted sẽ được xử lý bởi queue jobs nên ta cần có worker thực hiện nó

Tiếp đó ta mở resources/js/echo.jssửa lại chỗ Authorization như sau:

import Echo from "laravel-echo";
import Pusher from "pusher-js";
import axios from "axios";

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: "reverb",
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
    authorizer: (channel, options) => {
        return {
            authorize: (socketId, callback) => {
                axios
                    .post(
                        "/api/broadcasting/auth",
                        {
                            socket_id: socketId,
                            channel_name: channel.name,
                        },
                        {
                            headers: {
                                Authorization: `Bearer ${window.__app__.token}`,
                            },
                        }
                    )
                    .then((response) => {
                        callback(false, response.data);
                    })
                    .catch((error) => {
                        callback(true, error);
                    });
            },
        };
    },
});

Các bạn để ý rằng thay vì dùng localStorage thì ta đã lấy luôn từ biến window, giá trị được trả về từ AppController khi nãy

Ban đầu tính dùng localStorage mà sau dùng cách này tiện hơn 🤣🤣. Nhưng chú ý rằng đây chỉ là demo thôi nhé, tuyệt vời nhất là các bạn nên thực hiện đúng OAuth flow

Âu cây rồi, giờ ta F5 trình duyệt sẽ thấy /api/broadcasting/auth, thành công:

Screenshot 2024-11-26 at 12.06.46 AM.jpg

Terminal phía Reverb báo như sau là oke nè:

Screenshot 2024-11-26 at 12.09.32 AM.jpg

Giờ thì ta mở 2 tab trình duyệt, login 2 tài khoản chat với nhau là ngon luôn nha 😘😘😘:

ezgif-4-d7f159e0d1.gif

Buggg

Ầu... có gì lạ lạ à nha.

Đúng là từ A gửi tin nhắn sang B thì phía B realtime rồi, cơ mà phía A cũng nhận được tin nhắn, dẫn đến tình trạng tin nhắn bị lặp.

Ê mà ở phía Laravel ta có boadcast()->toOthers rồi mà ta 🤔🤔🤔

Đọc lại documentation của Laravel tí coi nha: https://laravel.com/docs/master/broadcasting#only-to-others-configuration

Àaaaaaaa...thì ra là ta phải gửi cả socketId theo request (axios) lên cho Laravel nữa, thì khi broadcast nó mới bỏ qua socketId của người gửi (sender)

Tức là ở file Room.vue -> saveMessage(), đoạn axios.post ta phải sửa như sau:

const saveMessage = async (content) => {
  try {
    const response = await axios.post('/messages', {
      room_id: parseInt(route.params.roomId),
      content
    }, {
      headers: {
        'X-Socket-ID': window.Echo.socketId(),
      }
    })
    messages.value.push(response.data.message)
    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

Ta lưu lại và quay lại trình duyệt F5 là sẽ thấy oke nha, không còn hiện tượng bị lặp tin nhắn 👍️👍️

Ấy thế cơ mà ở ứng dụng thật thì ta có thể có rất nhiều API mà ta cần boadcast()->toOthers, chả nhẽ cứ phải nhớ truyền header kia vào thì rồi có ngày quên xong lỗi ở production là mệt à nha 😂😂

Ta để ý rằng, khi tạo project mới thì ở file resources/js/bootstrap.js, ta đã có sẵn đoạn:

import axios from 'axios';
window.axios = axios;

Vậy thì giờ ta có thể tận dụng, ở mọi component VueJS, thay vì import axios from 'axios' thì ta dùng chung 1 instance axios global window.axios, sau đó set X-Socket-ID 1 lần là được 😉

Ta mở file resources/js/echo.jsvà sửa lại như sau:

import Echo from "laravel-echo";
import Pusher from "pusher-js";

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: "reverb",
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
    enabledTransports: ["ws", "wss"],
    authorizer: (channel, options) => {
        window.axios.defaults.headers.common["X-Socket-Id"] =
            window.Echo.socketId();
        return {
            authorize: (socketId, callback) => {

                window.axios
                    .post(
                        "/api/broadcasting/auth",
                        {
                            socket_id: socketId,
                            channel_name: channel.name,
                        },
                        {
                            headers: {
                                Authorization: `Bearer ${window.__app__.token}`,
                            },
                        }
                    )
                    .then((response) => {
                        console.log(1111, response.data);
                        callback(false, response.data);
                    })
                    .catch((error) => {
                        callback(true, error);
                    });
            },
        };
    },
});

Chú ý rằng mình đã bỏ đi đoạn import axios, cùng với đó là ở authorizer thì mình có lấy ra window.Echo.socketId() và set nó vào header mặc định của axios global luôn, và kể từ đây trở đi, mọi nơi mà ta dùng window.axios... sẽ tự có header kia.

Tiếp theo ở các component Vue nơi ta có dùng axios thì ta chuyển qua dùng window.axios nhé, ví dụ ở Room.vue

const getMessages = async () => {
  try {
    const response = await window.axios.get(`/messages?room_id=${route.params.roomId}`)
    messages.value = response.data
    scrollToBottom(false)
  } catch (error) {
    console.log(error)
  }
}

const saveMessage = async (content) => {
  try {
    const response = await window.axios.post('/messages', {
      room_id: parseInt(route.params.roomId),
      content
    })
    messages.value.push(response.data.message)
    scrollToBottom()
  } catch (error) {
    console.log(error)
  }
}

Ta thử gửi 1 message và inspect API kiểm tra header xem nha:

Screenshot 2024-11-28 at 1.07.08 PM.jpg

Presence Channel

Giờ để hiển thị danh sách user đang có trong phòng thì ta sẽ dùng Presence Channel nhé

Presence channel cũng chính là Private channel nhưng ở đó ta lấy được thông tin cụ thể từng user

Ở file routes/channels.php, ta sửa lại như sau nhé:

Broadcast::channel('room.{id}', function ($user, $id) {
    // // giờ đây ta trả về thông tin user chứ không trả về true/false nữa
    return $user;
});

Tiếp theo ta quay lại file app/Events/MessagePosted.php và sửa lại hàm broadcastOn như sau:

public function broadcastOn()
{
    return new PresenceChannel('room.'.$this->message->room_id);
}

Sau đó ta quay lại file Room.vuecreated ta sửa lại như sau:

onBeforeMount(() => {
  getMessages()
  const index = rootData.rooms.findIndex(item => item.id === parseInt(route.params.roomId))
  if (index > -1) {
    currentRoom.value = rootData.rooms[index]

    Echo.join(`room.${currentRoom.value.id}`)
      .here((users) => { // gọi ngay thời điểm ta join vào phòng, trả về tổng số user hiện tại có trong phòng (cả ta)
        usersOnline.value = users
      })
      .joining((user) => { // gọi khi có user mới join vào phòng
        usersOnline.value.push(user)
      })
      .leaving((user) => { // gọi khi có user rời phòng
        const index = usersOnline.value.findIndex(item => item.id === user.id)
        if (index > -1) {
          usersOnline.value.splice(index, 1)
        }
      })
      .listen('MessagePosted', (e) => {
        messages.value.push(e.message)

        scrollToBottom()
      })
  }
})

Giờ ta đóng tất cả terminal đi và start mới:

php artisan serve

npm run dev

php artisan reverb:start --debug

php artisan queue:work

Ngay sau đó các bạn load lại trang, và BÙM, ta có kết quả như sau:

Screenshot 2024-11-25 at 9.35.47 PM.jpg

Custom broadcasting

Hiện tại thì toàn bộ phần xử lý broadcasting ta gần như không có đụng vào mấy, chỉ đơn giản là đưa nó vào file routes/api.php, kiểu giống như lấy route API làm wrapper vậy thôi 😄

Trong trường hợp ta muốn custom phần xử lý broadcast thì có thể làm như sau, ở file routes/api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Log;

Broadcast::routes(['middleware' => ['auth:api']]);

Route::post('/broadcasting/auth', function (Request $request) {
    Log::info($request->all());
    
    // Viết bất kì logic nào bạn muốn ở đây

    return Broadcast::auth($request);
})->middleware('auth:api');

Ở trên ta vẫn return về Broadcast::auth($request); để Laravel làm nhiệm vụ xác thực channel, nhưng ta đã có thể viết thêm logic mà ta muốn ở đó. Ví dụ trên mình đơn giản là log ra tất cả các input được gửi kèm request (request body)

Ta quay trở lại trình duyệt F5, vô phòng chat, sau đó check phía server ở folder storage/logs/laravel.log, kéo xuống dưới cùng thấy như sau là oke nà 😊:

[2024-11-28 06:04:52] local.INFO: array (
  'socket_id' => '20030323.63677467',
  'channel_name' => 'presence-room.1',
)  
[2024-11-28 06:04:52] local.INFO: array (
  'socket_id' => '197200485.131181659',
  'channel_name' => 'presence-room.1',
)  
[2024-11-28 06:05:02] local.INFO: array (
  'socket_id' => '787640536.942947896',
  'channel_name' => 'presence-room.1',
)  
[2024-11-28 06:05:02] local.INFO: array (
  'socket_id' => '100173781.438157844',
  'channel_name' => 'presence-room.1',
)  
[2024-11-28 06:05:02] local.INFO: array (
  'socket_id' => '864390934.55836967',
  'channel_name' => 'presence-room.1',
)  

Nếu bạn gặp lỗi

Trong khi code nếu có khi nào bạn gặp lỗi thì xem lại bài đầu tiên trong series này của mình phần debug mình đã ghi rấttttttt là tâm huyết rồi các bạn à 😘😘😘

Kết bài

Lâu lắm mới có 1 bài "tàu nhanh" như thế này 😎😎😎.

Qua bài này các bạn thấy rằng việc dùng JWT Token để xác thực với Laravel Echo Server cũng không khó khăn lắm phải không nào? Nhưng vì mình thấy tài liệu và các bài tutorial trên mạng chủ yếu dùng session (như mình làm ở các bài trước), nên khi vào dự án thực tế mà dùng JWT thì bối rối không biết làm như thế nào, thì mong rằng qua bài này các bạn đã hiểu hơn về cách sử dụng nó.

Chúc các bạn thành công và hẹn gặp lại các bạn ở những bài sau


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí