JWT
JWT đã trở thành một trong những phương thức xác thực giao tiếp máy chủ hiện đại phổ biến nhất với đặc điểm nhẹ và không trạng thái.
Giới thiệu
JWT tên đầy đủ là JSON Web Tokens, về chi tiết giới thiệu của JWT có thể xem tại jwt.io, nó là một phương thức mở, an toàn, nhỏ gọn, truyền tải thông tin giữa hai bên dịch vụ với đối tượng là JSON, đặc điểm của nó là bảo mật cao, chống giả mạo nội dung, tiêu thụ thấp.
Cấu trúc
Trong tiêu chuẩn RFC, JWT bao gồm các phần sau:
- Header (Phần đầu)
- Payload (Phần tải)
- Signature (Chữ ký)
Mỗi phần được phân cách bằng dấu chấm ., cuối cùng tạo thành một chuỗi, định dạng là header.payload.signature, đây là cấu trúc tiêu chuẩn của một JWT token, tiếp theo sẽ giải thích tác dụng của từng cấu trúc.
TIP
Cần lưu ý rằng base64 và base64URL không phải là cùng một phương thức mã hóa, cái sau tương thích với URL web và đã được mã hóa escape.
Header
Phần đầu chỉ khai báo một số thông tin cơ bản, thường bao gồm hai phần, loại token và thuật toán mã hóa được sử dụng cho chữ ký, ví dụ như sau:
{
"alg": "HS256",
"typ": "JWT"
}Thông tin trên đại khái là, loại token là JWT, thuật toán mã hóa được sử dụng cho phần chữ ký là HS256, cuối cùng đối tượng JSON sẽ được mã hóa thành chuỗi thông qua Base64Url, chuỗi này chính là phần đầu của JWT.
Payload
Phần thứ hai của JWT là phần payload, chủ yếu chứa các phần khai báo (claims), phần khai báo thường là dữ liệu về một thực thể, ví dụ như một người dùng. Về loại khai báo tổng cộng có ba loại:
registered:Registered claimsđại diện cho một số khai báo được định nghĩa trước, một số không bắt buộc sử dụng nhưng vẫn được khuyến nghị sử dụng, ví dụ như:iss(người phát hành),exp(thời gian hết hạn),aud(đối tượng).public:Public claimscó thể được người sử dụng JWT định nghĩa tùy ý, tốt nhất là tránh xung đột với các phần khai báo khác.private claims: Phần khai báo này cũng là tùy chỉnh, thường được sử dụng để chia sẻ thông tin giữa các dịch vụ.
Một ví dụ về payload như sau:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}Đối tượng JSON này sẽ được mã hóa thành chuỗi thông qua Base64Url, từ đó tạo thành phần thứ hai của JWT.
DANGER
Mặc dù phần payload cũng được bảo vệ và chống giả mạo, nhưng phần này có thể đọc công khai, vì vậy không nên đặt thông tin nhạy cảm trong JWT.
Signature
Sau khi có được phần header và payload đã mã hóa, có thể sử dụng thuật toán chữ ký được chỉ định trong header để mã hóa chữ ký dựa trên nội dung của hai phần trước cộng với khóa bí mật, vì vậy một khi nội dung của JWT có bất kỳ thay đổi nào, chữ ký nhận được khi giải mã sẽ khác nhau, đồng thời nếu sử dụng khóa riêng tư, cũng có thể xác minh người phát hành JWT.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Ví dụ như ví dụ dưới đây:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)Cuối cùng nhận được một chuỗi gồm ba chuỗi base64Url được phân cách bằng ., đại khái như sau:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQNguyên lý hoạt động
Trong xác thực danh tính, khi người dùng đăng nhập thành công bằng thông tin đăng nhập, sẽ trả về một JSON Web token. Vì token là chứng chỉ, nên phải rất cẩn thận để tránh các vấn đề bảo mật. Nhìn chung, thời gian lưu trữ token không nên vượt quá thời gian cần thiết. Sau đó bất cứ khi nào người dùng muốn truy cập các route và tài nguyên được bảo vệ, khi gửi yêu cầu phải mang theo token, thường là trong header Authorization của header yêu cầu với Bearer schema, ví dụ như sau:
Authorization: Bearer <token>Sau khi máy chủ nhận được JWT, sẽ xác minh tính hợp lệ của nó, ví dụ như nội dung bị giả mạo, token đã hết hạn, v.v., nếu xác minh thành công thì có thể truy cập tài nguyên một cách suôn sẻ. Mặc dù trong JWT có thể mang theo một số thông tin cơ bản, nhưng vẫn khuyến nghị thông tin không nên quá lớn.
Thư viện JWT
Kho lưu trữ chính thức: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Tài liệu chính thức: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Thư viện này hỗ trợ phân tích và xác minh cũng như tạo và ký JWT. Hiện tại các thuật toán ký được hỗ trợ bao gồm HMAC SHA, RSA, RSA-PSS, và ECDSA, nhưng cũng có thể thêm hook của riêng mình.
Cài đặt
go get -u github.com/golang-jwt/jwt/v4Nhập
import "github.com/golang-jwt/jwt/v4"Chọn thuật toán chữ ký
Có nhiều thuật toán chữ ký khả dụng, trước khi sử dụng nên tìm hiểu sự khác biệt giữa chúng để chọn thuật toán chữ ký tốt hơn, sự khác biệt lớn nhất giữa chúng là mã hóa đối xứng và mã hóa bất đối xứng.
Thuật toán mã hóa đối xứng đơn giản nhất HSA, cho phép bất kỳ []byte nào cũng có thể được sử dụng làm khóa hợp lệ, vì vậy tốc độ tính toán nhanh hơn một chút. Khi cả nhà sản xuất và người tiêu dùng đều có thể được tin tưởng, hiệu quả của thuật toán mã hóa đối xứng là cao nhất. Tuy nhiên, vì ký và xác minh đều sử dụng cùng một khóa, nên không dễ dàng phân phối khóa dùng để xác minh, dù sao khóa ký cũng là cùng một khóa, nếu khóa ký bị rò rỉ thì tính bảo mật của JWT sẽ vô nghĩa.
Phương pháp ký mã hóa bất đối xứng, ví dụ như RSA, sử dụng các khóa khác nhau để ký và xác minh token, điều này làm cho việc tạo token với khóa riêng tư trở nên khả thi, đồng thời cho phép bất kỳ ai sử dụng khóa công khai để xác minh truy cập bình thường.
Các thuật toán ký khác nhau yêu cầu các loại khóa khác nhau, dưới đây là một số loại thuật toán ký phổ biến:
HMAC: Mã hóa đối xứng, cần giá trị loại[]byteđể ký và xác minh. (HS256,HS384,HS512)RSA: Mã hóa bất đối xứng, cần giá trị loại*rsa.PrivateKeyđể ký, và giá trị loại*rsa.PublicKeyđể xác minh.(RS256,RS384,RS512)ECDSA: Mã hóa bất đối xứng, cần giá trị loại*ecdsa.PrivateKeyđể ký, và giá trị loại*ecdsa.PublicKeyđể xác minh.(ES256,ES384,ES512)EdDSA: Mã hóa bất đối xứng, cần giá trị loạied25519.PrivateKeyđể ký và giá trị loạied25519.PublicKeyđể xác minh.(Ed25519)
Ví dụ
Dưới đây sẽ trình bày một số ví dụ, về tạo và ký jwt, cũng như phân tích và xác minh.
type Token struct {
Raw string // Chuỗi Token gốc, điền trường này khi bắt đầu phân tích
Method SigningMethod // Phương thức được sử dụng cho chữ ký
Header map[string]interface{} // Phần header của JWT
Claims Claims // Phần payload của JWT
Signature string // Phần chữ ký của JWT, điền khi bắt đầu phân tích
Valid bool // JWT có hợp lệ không
}Cấu trúc Token đại diện cho một JWT Token, việc sử dụng các trường chủ yếu phụ thuộc vào JWT được tạo/ký hoặc phân tích/xác minh như thế nào.
type RegisteredClaims struct {
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
}Đây là các Claims được định nghĩa trước mà thư viện cung cấp, có thể sử dụng phù hợp để đạt được mục đích yêu cầu.
Ví dụ 1. Tạo và ký HMAC
func TestHmac(t *testing.T) {
// Loại khóa bí mật của hmac là mảng byte
secret := []byte("my secret")
// Sử dụng thuật toán HS256, jwt.MapClaims là payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// Ký
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Đầu ra:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Ví dụ 2. Sử dụng Claims được định nghĩa trước
mySigningKey := []byte("AllYourBase")
// Tạo Claims
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)Đầu ra:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Ví dụ 3. Claims tùy chỉnh
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Tạo khóa bí mật
secret := []byte("my secret")
// Tạo Claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
// Tạo Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Ký
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Đầu ra:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Khi nhúng Claims tiêu chuẩn trong Claims tùy chỉnh, cần đảm bảo:
Claims tiêu chuẩn được nhúng là loại không phải con trỏ
Nếu là loại con trỏ, tốt nhất nên đảm bảo phân bổ bộ nhớ phù hợp trước khi truyền, nếu không sẽ panic.
Ví dụ 4. Phân tích và xác minh Token HMAC
func TestParse(t *testing.T) {
secret := []byte("my secret")
// Giả sử sử dụng thuật toán HS256 để tạo và ký đã tạo ra một token
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Truyền chuỗi token và hàm hook xác minh, giá trị trả về là một cấu trúc Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Xác minh thuật toán chữ ký có khớp không
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Thuật toán chữ ký không khớp: %s", token.Header["alg"])
}
// Trả về khóa xác minh
return secret, nil
})
if err != nil {
fmt.Println(token, err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Đầu ra:
map[id:123456 name:jack]Ví dụ 5. Xử lý lỗi
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// Giả sử sử dụng thuật toán HS256 để tạo và ký đã tạo ra một token
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Truyền chuỗi token và hàm hook xác minh, giá trị trả về là một cấu trúc Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Xác minh thuật toán chữ ký có khớp không
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Thuật toán chữ ký không khớp: %s", token.Header["alg"])
}
// Trả về khóa xác minh
return secret, nil
})
if token.Valid {
fmt.Println("token hợp lệ")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("Chuỗi được truyền vào thậm chí không phải là một token...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("token đã hết hạn hoặc chưa có hiệu lực")
} else {
fmt.Println("Xử lý token bất thường...")
}
}Đầu ra:
token hợp lệVí dụ 6. Phân tích Claims tùy chỉnh
Nếu sử dụng Claims tùy chỉnh khi tạo Token, thì khi phân tích nếu hy vọng Claims có thể chuyển đổi thành Claims tùy chỉnh thay vì map, cần truyền vào Claims tùy chỉnh.
func TestCustomClaimsParse(t *testing.T) {
secret := []byte("my secret")
tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"})) // Sử dụng option để xác minh
// Khẳng định kiểu
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Đầu ra:
&{114514 {Server [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}Ví dụ 7. Ký và phân tích RSA
RSA được sử dụng nhiều trong kiến trúc phân tán, quá trình đại khái như sau:
- Trung tâm xác thực tạo cặp khóa, sử dụng khóa riêng tư để ký jwt, jwt được trả về cho máy khách, khóa công khai do dịch vụ kinh doanh nắm giữ
- Máy khách mang jwt gửi yêu cầu đến dịch vụ kinh doanh, mô-đun kinh doanh sử dụng khóa công khai để phân tích jwt, không cần truy cập trung tâm xác thực
- Nếu xác thực thành công thì trả về thông tin kinh doanh
- Nếu xác thực thất bại thì trả về thông tin thất bại
func TestRsa(t *testing.T) {
// Tạo cặp khóa
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
if err != nil {
fmt.Println(err)
return
}
// claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Mã hóa khóa riêng tư
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Giải mã khóa công khai
token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
fmt.Println(err)
} else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
}
}Best Practices
- Không lưu trữ thông tin nhạy cảm: JWT payload có thể được đọc bởi bất kỳ ai, không lưu trữ mật khẩu hoặc thông tin nhạy cảm khác
- Sử dụng HTTPS: Luôn sử dụng HTTPS để truyền tải JWT
- Thiết lập thời gian hết hạn ngắn: Token nên có thời gian hết hạn ngắn để giảm rủi ro bảo mật
- Xác minh chữ ký: Luôn xác minh chữ ký trước khi tin tưởng JWT
- Sử dụng thuật toán mạnh: Sử dụng các thuật toán mã hóa mạnh như RS256 hoặc ES256 cho sản xuất
Kết luận
JWT là một phương thức xác thực mạnh mẽ và linh hoạt cho các ứng dụng Go hiện đại. Với sự hỗ trợ cho nhiều thuật toán ký khác nhau và khả năng tích hợp dễ dàng, JWT là lựa chọn tuyệt vời cho xác thực API và giao tiếp giữa các dịch vụ.
