Xây Dựng Hệ Thống Xác Thực Người Dùng Sử Dụng JWT Trong Golang (Phần 1)

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Qua 1 vòng Google để tìm hiểu về JWT thì không thấy ông Việt Nam nào nói cho tử tế và đầy đủ để 1 thằng newbie như mình hiểu. Sau đó vô tình mình tìm kiếm một bài viết mình cho là ổn nhất về JWT, nên mạo phép vừa dịch vừa sử dụng theo ý mình, những ai đọc được tiếng Anh có thể xem ở đây cho chuẩn: https://medium.com/swlh/building-a-user-auth-system-with-jwt-using-golang-30892659cc0

Vậy JWT là gì?

Chúng ta đều biết rằng, để nâng cao tính bảo mật khi làm các ứng dụng client-server thì cần có các cơ chế sinh token nhằm xác định request hợp lệ, tránh tình huống bị tấn công bởi các hacker vào hệ thống.

JWT = JSON Web Token, hiểu nôm na là xác thực người dùng từ phía máy chủ bằng JSON.

Theo cách tiếp cận truyền thống, chúng ta có các sessions để xác thực người dùng, khi mà người dùng đăng nhập thành công thì sẽ tạo ra 1 token lưu trên máy chủ để xác thực cho các request tiếp theo. Server sẽ gửi sessions ID cho client, và client sẽ gửi ID này kèm với các request sau đó, để server query trên database nhằm kiểm tra xem có đúng đó là request hợp lệ không.

Tuy nhiên, phương pháp này gặp nhiều khó khăn khi hệ thống cần scale lớn, xây dựng theo dạng Microservice với data lưu ở mỗi service khác nhau. Ví dụ bạn có n cụm máy chủ, thì để xác thực thành công, bạn phải truy vấn token lưu ở n máy chủ rồi so sánh, rất mất thời gian, cũng như dễ bị tấn công nếu như 1 máy chủ nào đó bị tấn công chiếm token. Tốt nhất là không lưu token trên máy chủ.

JWT ra đời nhằm giải quyết bài toán đó. Nó là phương pháp xác thực cho MSA(microservice architecture). Với cách tiếp cận này, thông tin xác thực chỉ lưu ở client-side. JWT đơn giản là 1 JSON payload để lưu định danh user đó. Nó đơn giản là 1 token chứa thông tin xác thực dạng Message Authentication Code(MAC), gồm 3 thành phần chính: header, payload và signature ngăn cách nhau bởi dấu chấm.

Ví dụ 1 token như sau:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Chúng ta thấy rất nhiều ký tự loạn xị ngậu lên, bản chất là đoạn string đã encode sang base64, ngăn cách nhau bởi các dấu chấm. Chúng ta vào trang jwt.io và copy cái token trên dán vào như sau:

Chúng ta thấy 1 token có 3 thành phần sau khi decoded như sau:

Phần header chứa metadata, bao gồm kiểu mã hóa của token, ở trên là HS256, dạng JWT. Để hiểu hơn các dạng mã hóa vui lòng tìm kiếm thêm từ khóa mã hóa công khai trên Google bạn nhé.

Phần Payload chứa thông tin xác định thằng user đó, ở trên là tên, sub, iat.

Phần còn lại là chữ ký. Chữ ký này là chữ ký điện tử các bạn nha, không phải là chữ ký khi bạn ký đơn kết hôn với vợ đâu, nhưng về mặt ý nghĩa thì nó cũng tương tự, là 1 khi đã ký thì bạn không thể thay đổi được nữa, bút sa gà chết.

Công thức ký như sau:

signature = hmacsha256(encoded_header + “.” + encoded_payload, “server_secret”)

hmacsha256 chính là 1 hàm băm sha 256, bạn hiểu nôm na là băm xong thì không chuyển ngược lại giữ liệu ban đầu, hay nói cách khác là 1 hàm 1 chiều, giống như bạn cho 1 con bò vào máy và tạo ra 1 cây xúc xích còn điều ngược lại thì không thể vậy đó. Hàm này băm header, encoded payload và khóa bí mật của server. Khái niệm băm cũng y chang bạn băm thịt vậy, cho xương, thịt.. mọi thứ và băm nhuyễn đến khi nào bạn lấy 1 mẩu trong đó ra thì nó có tính chất tương tự mọi nơi ở cục thịt sau khi băm.

Do vậy, khi chúng ta gửi 1 request gồm header và payload sau khi encoded trên thì phía server sẽ tiến hành băm chúng với khóa bí mật của server và tạo ra 1 chữ ký, rồi gửi lại cho client. Các lần request tiếp theo, nó cũng làm tương tự và tiến hành so sánh chữ ký mới nhất với chữ ký đã có trước đó, nếu như nó trùng nhau nghĩa là matches, request là hợp lệ. Luồng hoạt động của nó như sau:

  1. Client gửi thông tin đăng nhập gồm username và password.
  2. Server kiểm tra thông tin đó có tồn tại trong database không và tính hợp lệ của request.
  3. Nếu hợp lệ, JWT sẽ tạo 1 payload chứa thông tin định danh user và thời gian hiệu lực – expiration timestamp(chúng ta sẽ nói sau).
  4. Server sẽ tạo ra signature gồm 2 thành phần như đã nói ở trên. JWT đã hoàn thành xong 1 token gồm 3 thành phần header + payload + signature.
  5. Server sẽ gửi lại cho client thông tin token này để lưu lại, nhằm để client xác thực trong các request sau này.
  6. Với các request tiếp theo, server sẽ xác thực token bằng việc kiểm tra thời gian hiệu lực của token và tạo ra 1 chữ ký mới, sau đó so sánh với chữ ký đã sẵn có trong token.
How to secure your LoopBack 4 application with JWT authentication | LoopBack DocumentationSơ đồ hoạt động của JWT.

Vậy rõ ràng server không hề lưu token mà nó chỉ lưu duy nhất mỗi secret key để tạo ra chữ ký mà thôi. Cho nên nếu giả sử như chúng ta bị lộ khóa tới tay hacker thì server rất dễ bị tấn công. Do vậy chúng ta phải bảo mật khóa này. Có nhiều trường hợp commit cả khóa lên source code, dẫn đến lộ thông tin khóa mà server bị tấn công. Về phần này, các bạn có thể đọc thêm về cơ chế gitignore các file .env để không commit các thông tin nhạy cảm như khóa.

Cho đến thời điểm này khi nói về JWT thì chúng ta chỉ nói 1 khái niệm token chung. Nhưng thực tế chúng ta có 2 loại token, đó là AccessToken và RefreshToken.

Top việc làm Golang lương cao có tại TopDev

Tại sao lại có AccessToken và RefreshToken?

Bản chất 2 thằng trên đều là JSON Web Token, vậy tại sao lại cần tới 2 cái lận? Như bạn đã biết, JWT chứa đầy đủ thông tin cần thiết để xác thực, và hãy thử tượng tượng xem điều gì sẽ ra khì token này lọt vào 1 tay hacker nào đó. Hắn có thể sẽ giả mạo người dùng và bắt đầu làm chuyện tầm bậy với server của chúng ta như ăn cắp dữ liệu, tệ hơn là phá hỏng database. Do vậy người ta đẻ ra 1 cái gọi là access token, nhằm valid mỗi request của chúng ta, nó có đặc điểm là sẽ hết hạn sau 1 khoảng thời gian ngắn. Điều này nhằm hạn chế rủi ro khi hacker có được access token của bạn, họ cũng không phá hoại được nhiều.

Vậy thì khi access token này hết hạn, đồng nghĩa với việc user này cũng sẽ hết quyền truy vấn, do vậy chúng ta cần loại token thứ 2 để refresh lại token này, gọi là refresh token. Ở đây chúng ta cũng có thể check time expried của token, tuy nhiên giả sử như token hết hạn thì ta phải yêu cầu người dùng đăng nhập lại nhiều lần, làm cho người dùng khó chịu mà bỏ app, thì không ai muốn đúng không?

Refresh token này sẽ không bao giờ hết hạn hoặc hết hạn trong khoảng thời gian dài(ví dụ Google, github và Facebook thường là 90 ngày) và có nhiệm vụ tạo 1 access token mới từ Server. Tất nhiên, nếu bạn cũng để lộ refresh token này thì coi như toi, hacker lại có thể tấn công bạn được. Nhưng rõ ràng access token thì sử dụng với tuần suất lớn, còn refresh token thì chỉ khi nào access token hết hạn mới dùng đến thôi. Do vậy tăng tính bảo mật hơn đúng không nào?

What is the purpose of a “Refresh Token” ?Sơ đồ hoạt động của access token và refresh token.

Một ví dụ thực tế mà các hacker tấn công vào facebook, khi tạo ra các ứng dụng giả mạo hoặc các tool hỗ trợ facebook như xóa bạn bè, tự động like hay share. Họ yêu cầu người dùng cấp rất nhiều quyền, sau đó server facebook sẽ sinh ra 1 refresh token để có thể cấp quyền cho user. Khi họ có được nó, ai mà biết được họ đã làm những gì ngoài những tiện ích mà người dùng có được. Ví dụ như chạy quảng cáo chùa, tạo ra các tool để kiếm tiền trên facebook. Xem thêm tại đây:

https://vov.vn/cong-nghe/tin-moi/facebook-kien-4-nguoi-song-tai-viet-nam-tan-cong-chiem-doat-tai-khoan-870128.vov

Quay lại bài viết, sau khi user gửi thông tin username và password lên, server sẽ tạo ra 2 token gồm access token và refresh token và gửi trả lại cho client. Client gửi access token lên server để xác thực. Access token sẽ hết hạn trong thời gian ngắn, và client dùng refresh token để lấy 1 access token mới.

Hi vọng là bạn đã hiểu được ý nghĩa của JWT rồi. Trong phần tiếp theo chúng ta sẽ nghiên cứu cách triển khai JWT trong Golang.

Bài viết gốc được đăng tải tại codetoanbug.com

Xem thêm nhiều bài khác nữa:

Đừng bỏ lỡ top việc làm IT trên TopDev nhé!