Giới thiệu về JWT

JWT (JSON Web Token) là một phương pháp ngắn gọn, tự chứa để xác thực trên web. Nó bao gồm ba phần: phần header, phần payload và chữ ký. Phần header chứa thông tin về loại token và thuật toán mã hóa, phần payload chứa dữ liệu cần truyền (thông thường bao gồm một số thông tin quyền hạn và xác định người dùng), và chữ ký được sử dụng để xác minh tính toàn vẹn và tính hợp lệ của token.

JWT chủ yếu được sử dụng trong hai tình huống:

  1. Xác thực: Sau khi người dùng đăng nhập, mỗi yêu cầu tiếp theo sẽ bao gồm một JWT, cho phép người dùng truy cập các tuyến đường, dịch vụ và tài nguyên được phép.
  2. Trao đổi thông tin: JWT là một phương pháp tốt để truyền thông tin một cách an toàn giữa các bên, vì chúng có thể được ký số, ví dụ như sử dụng cặp khóa công khai / riêng tư.

Định dạng dữ liệu JWT

JWT là một cách ngắn gọn, an toàn trên URL để biểu diễn thông tin cần trao đổi giữa các bên. Thực tế, một JWT bao gồm ba phần, được ngăn cách bằng dấu chấm (.``), cụ thể là Header.Payload.Signature`. Tiếp theo, chúng ta sẽ trình bày chi tiết về định dạng dữ liệu của ba phần này.

1. Header

Header thường bao gồm hai phần: loại token—thông thường là JWT, và thuật toán chữ ký hoặc mã hóa được sử dụng, như HMAC SHA256 hoặc RSA. Header được biểu diễn dưới dạng JSON và sau đó được mã hóa thành một chuỗi sử dụng Base64Url. Ví dụ:

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

Sau khi mã hóa, bạn có thể nhận được một chuỗi tương tự như sau:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

Payload bao gồm một chuỗi các khai báo, đó là thông tin về thực thể (thông thường là người dùng) và dữ liệu khác. Payload có thể bao gồm nhiều khai báo được định nghĩa trước (cũng được biết đến là Khai Báo Đã Đăng Ký) và các khai báo tùy chỉnh (Khai Báo Riêng).

Khai báo đã định nghĩa trước có thể bao gồm:

  • iss (Issuer): Người phát hành
  • exp (Expiration Time): Thời gian hết hạn
  • sub (Subject): Chủ đề
  • aud (Audience): Đối tượng
  • iat (Issued At): Thời gian phát hành
  • nbf (Not Before): Thời gian có hiệu lực
  • jti (JWT ID): Định danh duy nhất của JWT

Một ví dụ về payload có thể trông như sau (dưới dạng JSON):

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

Thông tin này cũng sẽ được mã hóa với Base64Url, và bạn có thể nhận được một chuỗi tương tự như sau:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0

3. Chữ ký

Phần chữ ký được sử dụng để ký hai chuỗi đã mã hóa được đề cập ở trên, để xác minh rằng tin nhắn không bị thay đổi trong quá trình truyền tải. Đầu tiên, bạn cần xác định một khóa (nếu sử dụng thuật toán HMAC SHA256), sau đó sử dụng thuật toán được chỉ định trong header để ký chữ ký và payload.

Ví dụ, nếu bạn có header và payload sau:

HeaderEncoded.HeaderPayload

Mã giả cho việc ký chúng bằng một khóa có thể là:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)

Chuỗi đã ký có thể như sau:

dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

JWT Hoàn chỉnh

Kết hợp ba phần này với dấu chấm (.) làm phân tách sẽ tạo ra JWT hoàn chỉnh:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

Tiêu chuẩn JWT rất linh hoạt và có thể lưu trữ bất kỳ thông tin nào bạn cần trong payload (nhưng vì mục đích giảm kích thước JWT và vấn đề an toàn, thông tin nhạy cảm không nên được lưu trữ), và đảm bảo tính toàn vẹn của thông tin này qua chữ ký.

Cài đặt thư viện JWT cho Go

Cộng đồng Go cung cấp một gói github.com/golang-jwt/jwt để xử lý JWT. Để cài đặt thư viện này rất đơn giản, chỉ cần chạy lệnh sau trong thư mục dự án của bạn:

go get -u github.com/golang-jwt/jwt/v5

Sau khi cài đặt, bạn có thể nhập thư viện vào trong import như sau:

import "github.com/golang-jwt/jwt/v5"

Tạo một token đơn giản

Để tạo một token JWT đơn giản bằng ngôn ngữ Go và thuật toán HS256, bạn cần thực hiện các bước sau:

Đầu tiên, tạo một đối tượng Token mới, sử dụng thuật toán HS256:

token := jwt.New(jwt.SigningMethodHS256)

Tiếp theo, sử dụng phương thức SignedString để tạo một chuỗi biểu diễn của token này, truyền khóa mà bạn sẽ sử dụng để ký:

var mySigningKey = []byte("your-256-bit-secret")
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Đã xảy ra lỗi: %v", err)
}
fmt.Println(strToken)

Điều này sẽ tạo ra một token đơn giản mà không có bất kỳ claims nào.

Tạo một token với các tham số

Một trong những chức năng chính của JWT là chứa thông tin. Thông tin này được mã hóa trong Token thông qua các claims. Ví dụ, hãy tạo các claims tùy chỉnh:

// Cấu trúc Custom Claims
type MyClaims struct {
	jwt.RegisteredClaims
	Username string `json:"username"`
	Admin    bool   `json:"admin"`
}

// Tạo một token với các claims tùy chỉnh
claims := MyClaims{
    RegisteredClaims: jwt.RegisteredClaims{},
    Username: "username_của_bạn",
	Admin: true,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Khóa bí mật để ký
var mySigningKey = []byte("your-256-bit-secret")

// Tạo token dưới dạng chuỗi
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Đã xảy ra lỗi: %v", err)
}

fmt.Println(strToken)

Trong đoạn code trên, chúng ta đã xác định cấu trúc MyClaims để chứa các claims đã đăng ký cũng như một số thông tin tùy chỉnh. Sau đó, chúng ta vẫn sử dụng SignedString để tạo chuỗi token.

Phân tích cú pháp và xác thực token

Phân tích cú pháp và xác thực JWT rất quan trọng, và bạn có thể làm như sau:

// Chúng ta sẽ sử dụng cùng cấu trúc MyClaims
tokenString := "chuỗi-JWT-của-bạn"

// Chúng ta cần xác định một hàm mà gói jwt sẽ sử dụng để phân tích cú pháp tokenString
keyFunc := func(t *jwt.Token) (interface{}, error) {
	// Xác minh rằng phương pháp ký mong đợi được sử dụng
	if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
	    return nil, fmt.Errorf("Phương pháp ký không mong đợi: %v", t.Header["alg"])
	}
	// Trả về khóa bí mật cho token jwt, dưới dạng []byte, phù hợp với khóa đã sử dụng để ký trước đó
	return mySigningKey, nil
}

// Phân tích cú pháp token
claims := &MyClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
	log.Fatalf("Lỗi phân tích: %v", err)
}

if !parsedToken.Valid {
    log.Fatalf("Token không hợp lệ")
}

// Ở giai đoạn này, parsedToken đã được xác minh, và chúng ta có thể đọc các claims
fmt.Printf("Người dùng: %s, Admin: %v\n", claims.Username, claims.Admin)

Trong đoạn code trên, chúng ta cung cấp chuỗi token, một thể hiện MyClaims, và hàm khóa keyFunc cho hàm jwt.ParseWithClaims. Hàm này sẽ xác minh chữ ký và phân tích cú pháp token, điền vào biến claims nếu token hợp lệ.