[P3] Giải ngố authentication: JWT
Châm ngôn của mình là học để kiếm tiền.
Vì thế mình build các khóa học của mình để giúp anh em tiến bộ nhanh hơn x10 lần , để kiếm được nhiều tiền hơn
- 🏆 React.js Super: Trở thành React.js Developer trong 7 ngày với mức thu nhập 20 triệu/tháng
- 🏆 Node.js Super: Giúp bạn học cách phân tích, thiết kế, deploy 1 API Backend bằng Node.js
- 🏆 Next.js Super: Mình sẽ chia sẻ từ A-Z kiến thức về Next.js, thứ giúp mình kiếm hơn 1 tỉ/năm
Đây là phần 3 trong series Giải ngố authentication. Nếu các bạn chưa xem phần 1 và 2 thì có thể xem ở đây:
Phần này chúng ta sẽ đi tìm hiểu về JSON Web Token (JWT) - một phương pháp authentication khác được sử dụng rất nhiều trong các ứng dụng hiện đại ngày nay.
🥇JWT là gì?
JSON Web Token (JWT), phát âm là "jot" (giót 😂), là một chuẩn mở (RFC 7519) giúp truyền tải thông tin dưới dạng JSON.
Ở đây có một lưu ý là: Tất cả các JWT đều là token, nhưng không phải tất cả các token đều là JWT.
Sẵn tiện nếu bạn thắc mắc "Token là gì?" thì mình giải thích ngắn gọn như sau: Token là một chuỗi ký tự được tạo ra để đại diện cho một đối tượng hoặc một quyền truy cập nào đó, ví dụ như access token, refresh token, jwt... Token thường được sử dụng trong các hệ thống xác thực và ủy quyền để kiểm soát quyền truy cập của người dùng đối với tài nguyên hoặc dịch vụ.
Bởi vì kích thước tương đối nhỏ, JWT có thể được gửi qua URL, qua tham số POST, hoặc bên trong HTTP Header mà không ảnh hưởng nhiều đến tốc độ request.
Dưới đây là một JWT sau khi được encode và sign:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjQ0MTE4NDdhZmJkYjUxMmE1MmMwNTQ4IiwidHlwZSI6MCwiaWF0IjoxNjgyMDgyNTA0LCJleHAiOjE2OTA3MjI1MDR9.QjSI3gJZgDSEHz6eYkGKIQ6gYiiizg5C0NDbGbGxtWU
Cái chuỗi JWT trên có cấu trúc gồm ba phần, mỗi phần được phân tách bởi dấu chấm (.): Header, Payload và Signature.
Header: Phần này chứa thông tin về loại token (thường là "JWT") và thuật toán mã hóa được sử dụng để tạo chữ ký (ví dụ: HMAC SHA256 hoặc RSA). Header sau đó được mã hóa dưới dạng chuỗi Base64Url. (Thử decode Base64 cái chuỗi
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
này ra thì nó sẽ có dạng'{"alg":"HS256","typ":"JWT"}'
)Payload: Phần này chứa các thông tin mà người dùng định nghĩa. Payload cũng được mã hóa dưới dạng chuỗi Base64Url.
Signature: Phần này được tạo bằng cách dùng thuật toán HMACSHA256 (cái này có thể thay đổi) với nội dung là Base64 encoded Header + Base64 encoded Payload kết hợp một "secret key" (khóa bí mật). Signature (Chữ ký) giúp đảm bảo tính toàn vẹn và bảo mật của thông tin trong JWT (Công thức chi tiết nhìn xuống phía dưới nhé)
Bạn copy cái chuỗi trên và paste vào jwt.io thì sẽ thấy kết quả như sau
HEADER:ALGORITHM & TOKEN TYPE
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD:DATA
{
"user_id": "64411847afbdb512a52c0548",
"type": 0,
"iat": 1682082504,
"exp": 1690722504
}
VERIFY SIGNATURE
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret_key)
Lúc này bạn sẽ thắc mắc "Vậy tất cả mọi người đều biết được thông tin Header và Payload của cái JWT?"
Đúng rồi
Nhưng có một điều quan trọng là chỉ có server mới biết được secret_key để tạo ra Signature. Vì vậy chỉ có server mới có thể verify được cái JWT này là do chính server tạo ra.
Bạn không tin ư? Tôi đố bạn tạo ra được JWT như trên đó, dù bạn biết Header và Payload nhưng để tạo ra cái Signature thì bạn cần phải biết được secret_key của mình (nhìn c).
Mặc định thì JWT dùng thuật toán HMACSHA256 nên chúng ta yên tâm rằng JWT có độ an toàn cực cao và rất khó bị làm giả.
Hiểu được JWT rồi thì chúng ta cùng tìm hiểu về cách sử dụng JWT trong việc xác thực người dùng nhé.
🥇Xác thực người dùng với Access Token
Ở bài Session Authentication thì chúng ta được học rằng mỗi request lên server thì đều phải kèm theo session id để server có thể xác thực người dùng này là ai, có quyền truy cập tài nguyên hay không. Cái session id này được lưu ở cơ sở dữ liệu trên server, mỗi lần request phải mò vào đó kiểm tra xem session id này có trong đó không, rất mất thời gian.
🥈Access Token là gì?
Với JWT thì người ta phát hiện ra rằng chỉ cần tạo 1 cái token JWT, lưu thông tin người dùng vào như user_id
hay role
... rồi gửi cho người dùng, server không cần phải lưu trữ cái token JWT này làm gì. Mỗi lần người dùng request lên server thì gửi cái token JWT này lên, Server chỉ cần verify cái token JWT này là biết được người dùng này là ai, có quyền truy cập tài nguyên hay không.
💡 Mẹo:
Phương pháp dùng token để xác thực như thế này người ta gọi là Token Based Authentication.
Bạn sợ ai đó có thể làm giả cái token JWT của bạn hả?
Không! Không có ai có thể tạo ra được cái token JWT của bạn trừ khi họ biết cái secret_key của bạn, mà cái secret_key này bạn lưu trữ trên server mà, sao mà biết được (trừ bạn bị hack hay lỡ tay làm lộ thì chịu 🥲).
Vậy là chúng ta không cần lưu trữ cái JWT này trên server nữa, chỉ cần client lưu trữ là đủ rồi.
Tiết kiệm biết bao nhiêu là bộ nhớ cho server, mà còn nhanh nữa chứ (vì bỏ qua bước kiểm tra trong cơ sở dữ liệu, cái bước verify jwt thì nó nhanh lắm)
Và cái token ở trên để xác thực người dùng có quyền truy cập vào tài nguyên hay không người ta gọi là Access Token.
Access Token là một chuỗi với bất kỳ định dạng nào, nhưng định dạng phổ biến nhất của access token là JWT. Thường thì cấu trúc data trong access token sẽ theo chuẩn này. Tuy nhiên bạn có thể thay đổi theo ý thích, miễn sao phù hợp với dự án là được.
Đây là một chuỗi access token mẫu
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjQ0MTE4NDdhZmJkYjUxMmE1MmMwNTQ4IiwiaWF0IjoxNjgyMDgyNTA0LCJleHAiOjE2OTA3MjI1MDR9.tWlX7E7NPNftg37fXrdsXvkgEWB_8zaHIQmryAXzElY
Đây là payload của access token trên
{
"user_id": "64411847afbdb512a52c0548",
"iat": 1682082504,
"exp": 1690722504
}
Trong này có 3 trường quan trọng mà server dùng để kiểm tra token liệu có đúng người, hay còn hiệu lực không
user_id
: Chính là id định danh của người dùng, để biết token này là của người nàoiat
: Thời gian bắt đầu token này có hiệu lựcexp
: Thời gian kết thúc token này
Tùy từng trường hợp mà server có thể thêm các trường vào payload khi tạo access token, không cần cứng nhắc quá.
🥈Flow xác thực người dùng với Access Token
Hình dưới đây là luồng hoạt động của phương pháp xác thực bằng Access Token.
Client gửi request vào tài nguyên được bảo vệ trên server. Nếu client chưa được xác thực, server trả về lỗi 401 Authorization. Client gửi username và password của họ cho server.
Server xác minh thông tin xác thực được cung cấp so với cơ sở dữ liệu user. Nếu thông tin xác thực khớp, server tạo ra một JWT chứa payload là
user_id
(hoặc trường nào đó định danh người dùng). JWT này được gọi là Access Token.Server gửi access token cho client.
Client lưu trữ access token ở bộ nhớ thiết bị (cookie, local storage,...).
Đối với các yêu cầu tiếp theo, client gửi kèm access token trong header của request.
Server verify access token bằng secret key để kiểm tra access token có hợp lệ không.
Nếu hợp lệ, server cấp quyền truy cập vào tài nguyên được yêu cầu. Khi người dùng muốn đăng xuất thì chỉ cần xóa access token ở bộ nhớ thiết bị là được.
Khi access token hết hạn thì server sẽ từ chối yêu cầu của client, client lúc này sẽ xóa access token ở bộ nhớ thiết bị và chuyển sang trạng thái bị logout.
🥈Code Node.js mô phỏng flow xác thực người dùng bằng Access Token
// Tải các gói cần thiết
const express = require('express')
const jwt = require('jsonwebtoken')
// Tạo ứng dụng Express
const app = express()
// Sử dụng middleware express.json() để phân tích cú pháp các yêu cầu dạng JSON
app.use(express.json())
// Định nghĩa một secret key để mã hóa và giải mã token
const accessTokenSecret = 'yourAccessTokenSecret'
// Danh sách người dùng mẫu (thay thế bằng cơ sở dữ liệu thực tế)
const users = [
{
username: 'user1',
password: 'password1'
},
{
username: 'user2',
password: 'password2'
}
]
// Định nghĩa một middleware để xác thực access token
const authenticateToken = (req, res, next) => {
// Lấy access token từ header
const authHeader = req.headers['Authorization']
const token = authHeader && authHeader.split(' ')[1]
// Nếu không có token, trả về lỗi 401 (Unauthorized)
if (token == null) {
return res.sendStatus(401)
}
// Nếu có token, giải mã token và xác thực
jwt.verify(token, accessTokenSecret, (err, user) => {
// Nếu xảy ra lỗi hoặc token không hợp lệ, trả về lỗi 403 (Forbidden)
if (err) {
return res.sendStatus(403)
}
// Nếu token hợp lệ, tiếp tục xử lý yêu cầu
req.user = user
next()
})
}
// Định nghĩa route đăng nhập để tạo và cấp access token
app.post('/login', (req, res) => {
// Đọc thông tin đăng nhập từ yêu cầu
const { username, password } = req.body
// Tìm kiếm người dùng trong danh sách mẫu
const user = users.find((u) => u.username === username && u.password === password)
// Nếu không tìm thấy người dùng, trả về lỗi 401 (Unauthorized)
if (!user) {
return res.sendStatus(401)
}
// Nếu tìm thấy người dùng, tạo access token và trả về cho người dùng
const accessToken = jwt.sign({ username: user.username }, accessTokenSecret, { expiresIn: '1h' })
res.json({ accessToken })
})
// Tạo một route đơn giản để kiểm tra access token
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Bạn đã truy cập thành công nội dung được bảo vệ' })
})
// Khởi động máy chủ
app.listen(3000, () => {
console.log('Máy chủ đang chạy trên cổng 3000')
})
🥈Vấn đề của Access Token
Như flow trên thì chúng ta không lưu access token ở trên server, mà lưu ở trên client. Điều này gọi là stateless, tức là server không lưu trữ trạng thái nào của người dùng nào cả.
Khuyết điểm của nó là chúng ta không thể thu hồi access token được. Các bạn có thể xem một số ví dụ dưới đây.
Ví dụ 1: Ở server, chúng ta muốn chủ động đăng xuất một người dùng thì không được, vì không có cách nào xóa access token ở thiết bị client được.
Ví dụ 2: Client bị hack dẫn đến làm lộ access token, hacker lấy được access token và có thể truy cập vào tài nguyên được bảo vệ. Dù cho server biết điều đấy nhưng không thể từ chối access token bị hack đó được, vì chúng ta chỉ verify access token có đúng hay không chứ không có cơ chế kiểm tra access token có nằm trong danh sách blacklist hay không.
Với ví dụ thứ 2, chúng ta có thể thiết lập thời gian hiệu lực của access token ngắn, ví dụ là 5 phút, thì nếu access token bị lộ thì hacker cũng có ít thời gian để xâm nhập vào tài nguyên của chúng ta hơn => giảm thiểu rủi ro.
Nhưng mà cách này không hay lắm, vì nó sẽ làm cho người dùng bị logout và phải login sau mỗi 5 phút, rất khó chịu về trải nghiệm người dùng.
Lúc này người ta mới nghĩ ra ra một cách để giảm thiểu những vấn đề trên, đó là sử dụng thêm Refresh Token.
🥇Refresh Token là gì?
Refresh Token là một chuỗi token khác, được tạo ra cùng lúc với Access Token. Refresh Token có thời gian hiệu lực lâu hơn Access Token, ví dụ như 1 tuần, 1 tháng, 1 năm...
Flow xác thực với access token và refresh token sẽ được cập nhật như sau:
Client gửi request vào tài nguyên được bảo vệ trên server. Nếu client chưa được xác thực, server trả về lỗi 401 Authorization. Client gửi username và password của họ cho server.
Server xác minh thông tin xác thực được cung cấp so với cơ sở dữ liệu user. Nếu thông tin xác thực khớp, server tạo ra 2 JWT khác nhau là Access Token và Refresh Token chứa payload là
user_id
(hoặc trường nào đó định danh người dùng). Access Token có thời gian ngắn (cỡ 5 phút). Refresh Token có thời gian dài hơn (cỡ 1 năm). Refresh Token sẽ được lưu vào cơ sở dữ liệu, còn Access Token thì không.Server trả về access token và refresh token cho client.
Client lưu trữ access token và refresh token ở bộ nhớ thiết bị (cookie, local storage,...).
Đối với các yêu cầu tiếp theo, client gửi kèm access token trong header của request.
Server verify access token bằng secret key để kiểm tra access token có hợp lệ không.
Nếu hợp lệ, server cấp quyền truy cập vào tài nguyên được yêu cầu.
Khi access token hết hạn, client gửi refresh token lên server để lấy access token mới.
Server kiểm tra refresh token có hợp lệ không, có tồn tại trong cơ sở dữ liệu hay không. Nếu ok, server sẽ xóa refresh token cũ và tạo ra refresh token mới với expire date như cũ (ví dụ cái cũ hết hạn vào 5/10/2023 thì cái mới cũng hết hạn vào 5/10/2023) lưu vào cơ sở dữ liệu, tạo thêm access token mới.
Server trả về access token mới và refresh token mới cho client.
Client lưu trữ access token và refresh token mới ở bộ nhớ thiết bị (cookie, local storage,...).
Client có thể thực hiện các yêu cầu tiếp theo với access token mới (quá trình refresh token diễn ra ngầm nên client sẽ không bị logout).
Khi người dùng muốn đăng xuất thì gọi API logout, server sẽ xóa refresh token trong cơ sở dữ liệu, đồng thời client phải thực hiện xóa access token và refresh token ở bộ nhớ thiết bị.
Khi refresh token hết hạn (hoặc không hợp lệ) thì server sẽ từ chối yêu cầu của client, client lúc này sẽ xóa access token và refresh token ở bộ nhớ thiết bị và chuyển sang trạng thái bị logout.
🥈Vấn đề bất cập giữa lý thuyết và thực tế
Mong muốn của việc xác thực bằng JWT là stateless, nhưng ở trên các bạn để ý mình lưu refresh token vào cơ sở dữ liệu, điều này làm cho server phải lưu trữ trạng thái của người dùng, tức là không còn stateless nữa.
Chúng ta muốn bảo mật hơn thì chúng ta không thể cứng nhắc cứ stateless được, vậy nên kết hợp stateless và stateful lại với nhau có vẻ hợp lý hơn. Access Token thì stateless, còn Refresh Token thì stateful.
Đây là lý do mình nói có sự mâu thuẫn giữa lý thuyết và thực tế áp dụng, khó mà áp dụng hoàn toàn stateless cho JWT trong thực tế được.
Và có một lý do nữa tại sao mình lưu refresh token trong database đó là refresh token thì có thời gian tồn tại rất là lâu, nếu biết ai bị lô refresh token thì mình có thể xóa những cái refresh token của user đó trong database, điều này sẽ làm cho hệ thống an toàn hơn.
Tương tự nếu mình muốn logout một người dùng nào đó thì mình cũng có thể xóa refresh token của người đó trong database. Sau khoản thời gian access token họ hết hạn thì họ thực hiện refresh token sẽ không thành công và họ sẽ bị logout. Có điều là nó không tức thời, mà phải đợi đến khi access token hết hạn thì mới logout được.
Chúng ta cũng có thể cải thiện thêm bằng cách cho thời gian hết hạn access token ngắn lại và dùng websocket để thông báo cho client logout ngay lập tức.
🥇Trả lời một vạn câu hỏi vì sao về JWT
🥈Tại sao lại tạo một refresh token mới khi chúng ta thực hiện refresh token?
Vì nếu refresh token bị lộ, hacker có thể sử dụng nó để lấy access token mới, điều này khá nguy hiểm. Vậy nên dù refresh token có thời gian tồn tại rất lâu, nhưng cứ sau vài phút khi access token hết hạn và thực hiện refresh token thì mình lại tạo một refresh token mới và xóa refresh token cũ.
Lưu ý là cái Refresh Token mới vẫn giữ nguyên ngày giờ hết hạn của Refresh Token cũ. Cái cũ hết hạn vào 5/10/2023 thì cái mới cũng hết hạn vào 5/10/2023.
Cái này gọi là refresh token rotation.
🥈Làm thế nào để revoke (thu hồi) một access token?
Các bạn có thể hiểu revoke ở đây nghĩa là thu hồi hoặc vô hiệu hóa
Như mình đã nói ở trên thì access token chúng ta thiết kế nó là stateless, nên không có cách nào revoke ngay lập tức đúng nghĩa được mà chúng ta phải chữa cháy thông qua websocket và revoke refresh token
Còn nếu bạn muốn revoke ngay thì bạn phải lưu access token vào trong database, khi muốn revoke thì xóa nó trong database là được, nhưng điều này sẽ làm access token không còn stateless nữa.
🥈Có khi nào có 2 JWT trùng nhau hay không?
Có! Nếu payload và secret key giống nhau thì 2 JWT sẽ giống nhau.
Các bạn để ý thì trong payload JWT sẽ có trường iat
(issued at) là thời gian tạo ra JWT (đây là trường mặc định, trừ khi bạn disable nó). Và trường iat
nó được tính bằng giây.
Vậy nên nếu chúng ta tạo ra 2 JWT trong cùng 1 giây thì lúc thì trường iat
của 2 JWT này sẽ giống nhau, cộng với việc payload các bạn truyền vào giống nhau nữa thì sẽ cho ra 2 JWT giống nhau.
🥈Ở client thì nên lưu access token và refresh token ở đâu?
Nếu trình duyệt thì các bạn lưu ở cookie hay local storage đều được, mỗi cái đều có ưu nhược điểm riêng. Nhưng cookie sẽ có phần chiếm ưu thế hơn "1 tí xíu" về độ bảo mật.
Chi tiết so sánh giữa local storage và cookie thì mình sẽ có một bài viết sau nhé.
Còn nếu là mobile app thì các bạn lưu ở bộ nhớ của thiết bị.
🥈Gửi access token lên server như thế nào?
Sẽ có 2 trường hợp
- Lưu cookie: Nó sẽ tự động gửi mỗi khi request đến server, không cần quan tâm nó.
- Lưu local storage: Các bạn thêm vào header với key là
Authorization
và giá trị làBearer <access_token>
.
🥈Tại sao phải thêm Bearer vào trước access token?
Thực ra bạn thêm hay không thêm thì phụ thuộc vào cách server backend họ code như thế nào.
Để mà code api authentication chuẩn, thì server nên yêu cầu client phải thêm Bearer
vào trước access token. Mục đích để nói xác thực là "Bearer Authentication" (xác thực dựa trên token).
Bearer Authentication được đặt tên dựa trên từ "bearer" có nghĩa là "người mang" - tức là bất kỳ ai có token này sẽ được coi là người có quyền truy cập vào tài nguyên được yêu cầu. Điều này khác với các phương pháp xác thực khác như "Basic Authentication" (xác thực cơ bản) hay "Digest Authentication" (xác thực băm), cần sử dụng thông tin đăng nhập người dùng.
Việc thêm "Bearer" vào trước access token có một số mục đích chính:
Xác định loại xác thực: Cung cấp thông tin cho máy chủ về phương thức xác thực mà ứng dụng khách muốn sử dụng. Điều này giúp máy chủ xử lý yêu cầu một cách chính xác hơn.
Tính chuẩn mực: Sử dụng tiền tố "Bearer" giúp đảm bảo rằng các ứng dụng và máy chủ tuân theo các quy tắc chuẩn trong cách sử dụng và xử lý token.
Dễ phân biệt: Thêm "Bearer" giúp phân biệt giữa các loại token và xác thực khác nhau. Ví dụ, nếu máy chủ hỗ trợ nhiều phương thức xác thực, từ "Bearer" sẽ giúp máy chủ xác định loại xác thực đang được sử dụng dựa trên token.
Khi sử dụng Bearer Authentication, tiêu đề Authorization
trong yêu cầu HTTP sẽ trông như sau:
Authorization: Bearer your_access_token
🥈Khi tôi logout, tôi chỉ cần xóa access token và refresh token ở bộ nhớ của client là được chứ?
Nếu bạn không gọi api logout mà đơn thuần chỉ xóa access token và refresh token ở bộ nhớ của client thì bạn vẫn sẽ logout được, nhưng sẽ không tốt cho hệ thống về mặt bảo mật. Vì refresh token vẫn còn tồn tại ở database, nếu hacker có thể lấy được refresh token của bạn thì họ vẫn có thể lấy được access token mới.
🥈Tôi có nghe về OAuth 2.0, vậy nó là gì?
OAuth 2.0 là một giao thức xác thực và ủy quyền tiêu chuẩn dành cho ứng dụng web, di động và máy tính để bàn. Nó cho phép ứng dụng của bên thứ ba (còn gọi là ứng dụng khách) truy cập dữ liệu và tài nguyên của người dùng từ một dịch vụ nhà cung cấp (như Google, Facebook, Twitter, ...) mà không cần biết thông tin đăng nhập của người dùng.
Nói đơn giản, nó chỉ là một giao thức thôi, ứng dụng là làm mấy chức năng như đăng nhập bằng google, facebook trên chính website chúng ta á 😂.
Về cái này mình sẽ có một bài viết riêng luôn, vẫn trong series này nhé.
🥈Đến lượt câu hỏi của bạn
Nếu bạn có câu hỏi gì cứ comment, mình sẽ giải đáp và nếu câu hỏi hay mình sẽ thêm vào đây 😁
🥇Tổng kết
Bài này mình tưởng nó ngắn nhưng tính ra cũng dài phết, mong rằng nó giúp các bạn hiểu được tườm tận JWT là gì và cách nó hoạt động như thế nào.
Vẫn câu nói từ bài 1, mình tin rằng khi mọi người đọc hết series này của mình thì mọi người sẽ tự tin trả lời phỏng vấn về Authentication như JWT, OAuth 2.0, OpenID Connect, ... 😁
Oke, bài viết đến đây là hết rồi, hẹn mọi người ở bài sau nhé.
🥇Tham khảo thêm
Kiến thức trong khóa học Next.js này đã giúp mình kiếm hơn 1 tỉ đồng/năm
Phew! Cuối cùng bạn cũng đã đọc xong. Bài viết này có hơi dài một tí vì mình muốn nó đầy đủ nhất có thể 😅
Website bạn đang đọc được viết bằng Next.js TypeScript và tối ưu từng chi tiết nhỏ như SEO, hiệu suất, nội dung để đảm bảo bạn có trải nghiệm tốt nhất.
Với lượt view trung bình là 30k/tháng (dù website rất ít bài viết). Website này đem lại doanh thu 1 năm vừa qua là hơn 1 tỉ đồng
Đó chính là sức mạnh của SEO, sức mạnh của Next.js.
Mình luôn tin rằng kiến thức là chìa khóa giúp chúng ta đi nhanh nhất.
Mình đã dành hơn 6 tháng để phát triển khóa học Next.js Super | Dự án quản lý quán ăn & gọi món bằng QR Code. Trong khóa này các bạn sẽ được học mọi thứ về framework Next.js, các kiến thức từ cơ bản cho đến nâng cao nhất, mục đích của mình là giúp bạn chinh phục mức lương 25 - 30 triêu/tháng
Nếu bạn cảm thấy bài viết này của mình hữu ích, mình nghĩ bạn sẽ thích hợp với phong cách dạy của mình. Không như bài viết này, khóa học là sự kết hợp giữa các bài viết, video, bài tập nhỏ và dự án lớn có thể xin việc được ngay. Học xong mình đảm bảo bạn sẽ lên tay ngay. 💪🏻