understanding-JSON-Web-Tokens-more-depth-trungquandev

Hiểu sâu về JWT – JSON Web Tokens

Xin chào tất cả các bạn, mình là Quân, hôm nay chúng ta sẽ cùng nhau đi tìm hiểu về một thuật ngữ có tên là JWT – chữ viết tắt của “JSON Web Tokens”, để trả lời một vài câu hỏi như:
* JWT nó là cái gì?
* Tại sao phải dùng và khi nào thì dùng tới nó?
* Cách thức tạo ra nó cũng như JWT nó hoạt động ra làm sao?
* Cuối cùng là làm ví dụ luôn về nó nhé, mình sẽ sử dụng Nodejs cho phần demo với thằng JWT này.

“Bài này nằm trong loạt bài Lập Trình Nodejs từ cơ bản đến nâng cao trên trang blog chính thức trungquandev.com

Những nội dung có trong bài:

  1. Hoàn cảnh ra đời của JWT
  2. JWT hoạt động như thế nào?
  3. Cách tạo ra một mã JWT.
  4. JWT bảo vệ dữ liệu của chúng ta bằng cách nào?
  5. Server xác thực mã JWT gửi lên từ client ra sao?
  6. Làm ứng dụng demo JWT với Nodejs

1. Hoàn cảnh ra đời của JWT

Phần này sẽ trả lời cho các bạn đầy đủ về JWT nó là gì? Tại sao và khi nào cần dùng tới JWT như mình đã nói ở trên kia nhé. Ok let’s understanding…

Đối với bất kỳ một ứng dụng web, di động, desktop…vv chắc chắn các bạn đều đã từng tạo tài khoản, sau đó phải đăng nhập để sử dụng các tính năng bên trong của ứng dụng, hành động đó gọi là Authentication – xác thực người dùng.

Vậy thì xác thực người dùng bằng cách nào? Đầu tiên chúng ta sẽ nhìn lại một chút mô hình đơn giản về việc các trang web phổ biến xưa nay sử dụng một Session – Cookie để Authenticate nhé.

session-authentication-diagram-trungquandev

Đối với mô hình xác thực Session – Cookie như trên, khi một người dùng đăng nhập vào trang web thì server sẽ tạo ra một Session (phiên làm việc) cho người dùng đó và lưu ở Server, đồng thời ID của Session sẽ được trả về và lưu lại trên Cookie trình duyệt của người dùng.
(Session này sẽ có hạn sử dụng do Dev chúng ta chỉ định, ví dụ 1 ngày chẳng hạn. Sau một ngày thì người dùng phải đăng nhập lại để tạo ra một Session làm việc mới.)

Và khi người dùng vẫn đang đăng nhập, Session còn hạn, thì Cookie sẽ luôn luôn được đính kèm cùng với mọi Request tiếp theo của người dùng đó mỗi khi gửi lên Server.
Sau đó Server sẽ so sánh cái Session ID nhận được từ Cookie với thông tin Session lưu trên server (có thể là RAM hoặc Database) để xác minh người dùng và gửi lại Response tương ứng.

Ok, có vẻ mọi chuyện tới đây khá là suôn sẻ phải không? Không hề có vấn đề gì xảy ra, vậy tại sao JWT lại xuất hiện và dần được ưa chuộng hơn Session?

Câu trả lời đơn giản nằm ở việc “Extend Platform”mở rộng nền tảng hệ thống.

Bây giờ hãy thử tưởng tượng chúng ta đang có một trang web hoạt động ngon lành với Session rồi, nhưng tới một ngày chúng ta phải làm thêm các Native Apps (ứng dụng di động) cho hệ thống là IOS hoặc Android, và cùng sử dụng chung một Database với ứng dụng web hiện tại thì như thế nào?

Rõ ràng lúc này chúng ta sẽ không thể xác thực người dùng sử dụng Native app bằng Session được vì Native app không có Cookie, chỉ Browser mới có mà thôi.

Làm gì bây giờ? Không lẽ chúng ta lại viết thêm một project khác phía backend để hỗ trợ riêng Native apps? Rồi các tính năng thì giống y hệt với backend của ứng dụng web đang chạy Session hiện tại, như vậy phải viết code lặp lại 2 lần cho 2 project đối với mỗi tính năng sao? @.@

Hoặc nếu code cũ của con web app là “code xịn, rất clean” thì có thể viết thêm một module riêng cho việc authenticate những người đăng nhập bằng Native app, nhưng dù sao thì cách này cũng vừa phức tạp vừa vẫn dư thừa code ở việc xác thực.

Vậy nên JWT mới được sinh ra trên thế giới lập trình này để giải quyết vấn đề trên và hiện nay nó luôn được ưu tiên lựa chọn mỗi khi bắt đầu làm một dự án mới.

Bonus nguyên gốc khái niệm của JWT cho các bạn hại não trước khi sang tới phần tiếp theo chúng ta sẽ đào sâu hơn về cách JWT nó hoạt động ra sao nhé:

“A JSON Web Token (JWT) is a JSON object that is defined in RFC 7519 as a safe way to represent a set of information between two parties. The token is composed of a header, a payload, and a signature.”
— Mình tạm dịch: —
“JWT là một JSON object được định nghĩa trong chuẩn RFC 7519 như là một cách an toàn để trao đổi thông tin giữa hai bên. Và Token thì bao gồm một header, một payload và một chữ ký.”

Nếu chưa đủ hại não thì các bạn có thể đọc thêm ở đây nữa nhé:
https://tools.ietf.org/html/rfc7519


2. JWT hoạt động như thế nào?

Trong phần này, chúng ta sẽ nghiên cứu xem JWT nó xử lý authenticate như thế nào nhé, trước tiên, mình có một hình vẽ như thế này:

jwt-token-authentication-diagram-trungquandev

Sơ đồ trên cũng không khó để hiểu, chỉ đơn giản thay vì tạo session như ở mô hình ban nãy thì lần này Server sẽ ký một mã JWT sau đó gửi về cho phía client lưu trữ, những request tiếp theo từ client gửi lên thì phải đính kèm mã JWT này (thông thường là ở header), server sẽ check mã này và gửi lại response thành công hoặc thất bại tương ứng ngược về client.

Đối với việc lưu trữ JWT ở client, sẽ có vài trường hợp, nếu như là trình duyệt web thì JWT có thể lưu vào Local Storage, IOS app thì sẽ là Keychain và Android app sẽ lưu vào SharedPreferences.”


3. Cách tạo ra một mã JWT.

Mô tả như sơ đồ ở phần 2 thì khá dễ dàng để các bạn có thể nắm được tổng quát rồi, bây giờ chúng ta sẽ đi sâu hơn xem JWT nó thực sự được tạo ra như thế nào nhé:

“Từ phần này trở đi mình cần các bạn dừng hết mọi xao nhãng xung quanh, tập trung cao độ đọc để hiểu nha.
Let’s keep your mind is depth !”

Đầu tiên, để đào sâu vào mã JWT thì các bạn cần phải nắm được 3 thành phần quan trọng của nó lần lượt là: HEADER, PAYLOAD và SIGNATURE

HEADER: Đây là nơi chứa cái thông tin mà được dùng để trả lời cho câu hỏi: “Mã JWT được tính toán như thế nào?”
– Ví dụ cho một cái Header, nó là một đối tượng JSON giống như thế này:

{
  "typ": "JWT",
  "alg": "HS256"
}

Trong cái Header ví dụ trên, thì “typ” (viết tắt của type) là kiểu Token, ở đây chính là JWT.

Còn “alg” (viết tắt của algorithm) là thuật toán băm tạo ra chữ ký cho Token, ở ví dụ trên HS256 là thuật toán có tên HMAC-SHA256, một thuật toán băm sử dụng khóa bí mật (Secret Key) để tính toán tạo ra chữ ký.

PAYLOAD: Đây là nơi chứa những dữ liệu mà chúng ta muốn lưu lại trong JWT.
– Ví dụ mình muốn lưu một vài thông tin của thằng user vào token, như thế này:

{
  "userId": "7j79y-kdjr8n4h-5jd8-5k39-cfk8ghr9wu",
  "username": "trungquandev17",
  "occupation": "Full stack web developer",
  // standard fields
  "iss": "Trung Quan, author of blog: https://trungquandev.com",
  "iat": 1568456819,
  "exp": 1568460419
}

Trong ví dụ trên, mình lưu 3 thông tin của thằng user khi login thành công đó là id, tênnghề nghiệp, dĩ nhiên các bạn muốn lưu thêm bao nhiêu cũng được.

Ngoài ra để ý cho mình 3 cái trường mình để phía dưới dòng Standard Fields, chúng nó là những trường tiêu chuẩn, là optional nghĩa là các bạn có thể tạo hoặc không, nhưng mà nên tạo nhé vì nó sẽ hữu ích.

“iss” viết tắt của Issuer là thông tin người tạo ra Token (không phải user đâu nhé, mà nó chính là tên cái hệ thống backend của các bạn chẳng hạn)

“iat” viết tắt của Issued at, là nhãn thời gian lúc mà cái token được tạo.

“exp” viết tắt của Expiration time, xác định thời gian hết hạn của Token

Ngoài ra còn nhiều standard fields nữa, các bạn hãy tham khảo ở đây nhé, thấy cái nào phù hợp thì dùng cho ứng dụng của các bạn.
https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields

SIGNATURE: Cùng xem qua đoạn mã giả dưới đây rồi mình sẽ giải thích chi tiết về phần thực hiện lấy chữ ký này nhé:

// signature algorithm
const data = base64urlEncode(header) + “.” + base64urlEncode(payload);
const hashedData = Hash(data, secret);
const signature = base64urlEncode(hashedData);

Đầu tiên, chúng ta sẽ Encode (chuyển đổi) 2 cái HeaderPlayload ở trên theo kiểu Base64URL Encoder, và nối 2 chuỗi nhận được lại (cách nhau bởi dấu chấm “.”) rồi gán nó vào một biến là data.

Tiếp theo sẽ Hash (băm) cái data đó bằng “alg”, chính thuật toán tạo chữ ký mà chúng ta đã định nghĩa ở trên Header (HS256HMAC-SHA256) kèm với một chuỗi bí mật secret (chuỗi secret này sẽ được đặt tùy vào lập trình viên của mỗi dự án và đảm bảo không được để lộ chuỗi này ra ngoài, có thể đưa vào biến môi trường ENV.)

Sau khi băm xong ở trên thì thực hiện Encode tiếp một lần nữa cái dữ liệu băm đó dưới dạng Base64URL Encode, và chúng ta sẽ thu được chữ ký “Signature”.


Sau khi đã có đủ 3 thành phần HEADER, PAYLOAD, SIGNATURE thì làm gì? Bước này đặc biệt quan trọng, đó là chúng ta sẽ kết hợp 3 thành phần trên lại và phân cách chúng nó bởi dấu chấm theo đúng cấu trúc chuẩn của JWT: header.payload.signature

Cụ thể hơn, mình sẽ mô tả lại bằng đoạn mã giả này:

const headerEncode = base64urlEncode(header); // ví dụ kết quả: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

const payloadEncode = base64urlEncode(payload); // ví dụ kết quả: eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ

const data = headerEncode + "." + payloadEncode;
const hashedData = Hash(data, secret);
const signature = base64urlEncode(hashedData);  // ví dụ kết quả: xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

// cuối cùng thì mã JWT theo đúng cấu trúc header.payload.signature sẽ trông như sau:
const JWT = headerEncode + "." + payloadEncode + "." + signature;
// Kết quả: 
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM"

Khá rối não đúng không :v, yên tâm, ở phần làm demo JWT với NodeJS các bạn thực hành là sẽ dễ hiểu hơn ngay =))
Ngoài ra thì các bạn cũng có thể tự tạo nhanh một mã JWT bằng công cụ online ở đây:
https://jwt.io/


4. JWT bảo vệ dữ liệu của chúng ta bằng cách nào?

Câu trả lời đó là: JWT không bảo vệ dữ liệu của bạn.

Mục đích quan trọng mà các bạn cần phải nắm được là JWTkhông ẩn, không làm mờ, không che giấu dữ liệu gì cả, mà nó được sử dụng để chứng minh rằng dữ liệu được tạo ra bởi một nguồn xác thực.

Các bạn có thể nhìn lại ở các bước xử lý Header, Payload, Signature trên kia, dữ liệu chỉ được Encoded và Hash (Signed) chứ không phải Encrypted.

“Lưu ý: mình phải giữ nguyên mấy từ trên bằng tiếng anh bởi vì EncodedEncrypted dịch ra tiếng việt đều là “Mã hóa” nhưng bản chất ý nghĩa của chúng hoàn toàn khác nhau.”

Ngoài ra Các bạn có thể tham khảo ở 2 link dưới đây để hiểu thêm về sự khác nhau giữa Encoded và Encrypted nhé:
https://danielmiessler.com/study/encoding-encryption-hashing-obfuscation/#encoding
https://stackoverflow.com/a/4657456

Vậy thì có bạn sẽ đặt ra thắc mắc: “nếu một kẻ tấn công ở giữa (Man-in-the-middle) bắt được gói tin có chứa mã JWT rồi họ decode ra và lấy được thông tin của user thì sao?”

Câu trả lời là: “đúng, điều đó là có thể, vậy nên hãy luôn luôn đảm bảo rằng ứng dụng của các bạn chắc chắn phải có giao thức mã hóa đường truyền HTTPS nhé.


5. Server xác thực mã JWT gửi lên từ client ra sao?

Trong phần 3 trên kia các bạn hãy nhìn lại cho mình đó là khi tạo mã JWT, chúng ta có sử dụng tới một chuỗi bí mật “Secret” trong bước tạo chữ ký (signature).
Chuỗi “Secret” này là unique cho ứng dụng và phải được ưu tiên lưu trữ bảo mật cẩn thận ở phía server.

Khi nhận được mã Token gửi lên từ phía client, Server sẽ lấy phần Signature (chữ ký) bên trong mã token đó, và verify (kiểm tra ) xem cái chữ ký nhận được có đúng chính xác là được HASH (băm) bởi cùng một thuật toán và chuỗi “Secret” như trên hay không.
(Còn việc Verify như thế nào thì trong phần làm ví dụ code demo mình sẽ hướng dẫn cụ thể cho các bạn sau nhé)

Và cuối cùng, rõ ràng, nếu chữ ký của client gửi lên khớp với chữ ký được tạo ra từ máy chủ, thì cái JWT đó là hợp lệ, ngược lại thì không, và người lập trình API phía Backend như chúng ta sẽ tùy vào từng trường hợp mà response về cho client một cách hợp lý.

Ngoài ra, điều này rất quan trọng, đó là người dùng có kinh nghiệm lập trình họ vẫn có thể thêm hoặc sửa thông tin trong phần payload khi gửi lên server. Mình giả sử một rủi ro như sau:

Người dùng chuyển tiền, sau đó tới bước thanh toán, họ đổi cái id của họ phía bên trong token thành id của người khác và vẫn gửi lên kèm với đúng chữ ký thì sao?
Chính vì vậy mà ở phía server chúng ta cũng nên lưu lại token của người dùng trước khi gửi về cho client, để có thể đảm bảo được những dữ liệu sau này client truyền lên là đảm bảo hợp lệ.
Ngoài ra thì việc lưu lại token của người dùng ở phía Server cũng sẽ có lợi cho tính năng Force Logout người dùng khỏi hệ thống khi cần thiết.


6. Làm ứng dụng demo JWT với Nodejs

Ban đầu mình định gộp phần demo vào chung một bài viết, nhưng vì sau khi xử lý hết cái đống lý thuyết ở trên, bài viết này cũng khá dài rồi nên mình sẽ tách phần làm ví dụ Demo JWT với Nodejs sang một bài viết khác ở đây nhé:

[Link đang cập nhật]


Tổng kết lại, hôm nay chúng ta đã đào khá sâu vào JWT – JSON Web Token rồi, nếu có gì thắc mắc, các bạn hãy comment dưới bài viết này, mình sẽ check sớm nhất có thể nhé.


Cảm ơn các bạn đã dành thời gian đọc bài viết.

Xin chào và hẹn gặp lại các bạn ở những bài viết tiếp theo.

Best Regards – Trung Quân – Green Cat


Tham khảo kiến thức:

https://medium.com/vandium-software/5-easy-steps-to-understanding-json-web-tokens-jwt-1164c0adfcec
https://medium.com/@sherryhsu/session-vs-token-based-authentication-11a6c5ac45e4

“Thanks for awesome knowledges.”

trungquandev-img-modal

Khóa học lập trình làm việc thực tế:

Nếu các bạn thấy bài viết của mình có ích thì hãy ủng hộ mình bằng cách tham khảo bài viết giới thiệu khóa học cực kỳ chất lượng và chính chủ dưới đây của mình nhé, cảm ơn các bạn ^^

nodejs-mongodb-messenger-realtime-course-trungquandev
Node.js và MongoDB - Xây dựng một ứng dụng Messenger trò chuyện trực tuyến.