Thời gian qua, mình có phải vật lộn với 1 framework mới, mày mò trong tài liệu hướng dẫn thì thấy nó có đề cập tới 1 phương pháp xác thực quyền truy cập ( Authentication) bằng JSON Web Token (JWT). Sau khi đào sâu hơn về cái này, mình nhận thấy quả thực JWT nó còn tuyệt vời hơn ngoài mong đời và khả năng của nó sẽ không chỉ dừng lại ở mỗi Authentication. Qua bài viết này mình muốn giúp những ai còn chưa biết tới JWT hoặc chưa hiểu rõ về nó hình dung được mô tả trực quan nhất về JWT và những gì mà JWT có thể đem lại cho Web Service.

JSON Web Token là gì?

JSON Web Token (JWT) là 1 tiêu chuẩn mở ( RFC 7519) định nghĩa cách thức truyền tin an toàn giữa các thành viên bằng 1 đối tượng JSON. Thông tin này có thể được xác thực và đánh dấu tin cậy nhờ vào “chữ ký” của nó. Phần chữ ký của JWT sẽ được mã hóa lại bằng HMAC hoặc RSA.

mô hình 1 json web token

Ví dụ cho 1 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Những đặc điểm nổi bật của JWT:

  1. Kích thước nhỏ: JWT có thể được truyền thông qua URL, hoặc qua giao thức POST, hay nhét vào bên trong phần HTTP Header. Kích thước nhỏ hơn ứng với công việc truyền tải sẽ nhanh hơn. Dưới đây là cách thức truyền token vào trong HTTP Header sử dụng Bearer Schema

    Authorization: Bearer <token>
    
  2. Khép kín: Phần Payload (hiểu nôm na là khối hàng) chứa toàn bộ những thông tin mà chúng ta cần tới, ví dụ như thông tin của người dùng (thay vì phải truy vấn cơ sở dữ liệu nhiều lần)

Khi nào nên dùng JSON Web Token?

Dưới đây là 1 vài kịch bản thích hợp với JWT:

  • Authentication: Đây là kịch bản phổ biến nhất cho việc sử dụng JWT. Một khi người dùng đã đăng nhập vào hệ thống thì những request tiếp theo từ phía người dùng sẽ chứa thêm mã JWT, cho phép người dùng quyền truy cập vào các đường dẫn, dịch vụ, và tài nguyên mà cần phải có sự cho phép nếu có mã Token đó. Phương pháp này không bị ảnh hưởng bởi Cross-Origin Resource Sharing (CORS) do nó không sử dụng cookie.

    Client application                                            API
        --------                                              -----------
            |                                                      |
            |                   GET /api/employees                 |
            |----------------------------------------------------->|
            |                     403 Forbidden                    |
            |<-----------------------------------------------------|
            |                                                      |
            |                                                      |
            |                 POST /api/authenticate               |
            |     { login: "paduvi", password: "chotoxautinh" }    |
            |----------------------------------------------------->|
            |                      200 Success                     |
            |             { token: "my.personal.token" }           |
            |<-----------------------------------------------------|
            |                                                      |
            |                                                      |
            |                 GET /api/employees                   |
            | Header { "Authorization: Bearer "my.personal.token" }|
            |----------------------------------------------------->|
            |                      200 Success                     |
            |<-----------------------------------------------------|
            |                                                      |
    
    
  • Trao đổi thông tin: JSON Web Token là 1 cách thức không tồi để truyền tin an toàn giữa các thành viên với nhau, nhờ vào phần “chữ ký” của nó. Phía người nhận có thể biết được  người gửi là ai thông qua phần chữ ký. Ngoài ra, chữ ký được tạo ra bằng việc kết hợp cả phần header, payload lại nên thông qua đó ta có thể xác nhận được chữ ký có bị giả mạo hay không.

Đăng ký ngay hôm nay để xem toàn bộ các video bài giảng, mã nguồn dự án mẫu, hướng dẫn thực hành lập trình HTML5, CSS3, JavaScript

Cấu trúc của JSON Web Token:

JSON Web Token bao gồm 3 phần, được ngăn cách nhau bởi dấu chấm (.):

  1. Header
  2. Payload
  3. Signature (chữ ký)

Tổng quát thì nó có dạng như sau:

xxxxx.yyyyy.zzzzz

Hãy cùng nhau khám phá mỗi phần bên trong JWT nhé:

Header:

Phần Header dùng để khai báo kiểu chữ ký và thuật toán mã hóa sẽ dùng cho cái token của chúng ta.

Ví dụ cho phần Header:

{
  “alg”: “HS256”,
  “typ”: “JWT”
}

Đoạn Header này khai báo rằng đối tượng được mã hóa là 1 JWT (để phân biệt với JWS hay JWE), và chữ ký của nó sử dụng thuật toán mã hóa HMAC SHA-256.

Đoạn Header này sẽ được mã hóa base64url, và ta thu được phần đầu tiên của JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Chú ý rằng mình viết ở phía trên là base64url chứ không phải là base64. Về cơ bản 2 cái này là tương tự nhau nhưng giữa chúng vẫn có những sự khác biệt:

  • Không thêm = vào
  • Các ký tự +/ sẽ được thay thế bằng -****_

Các bạn có thể so sánh sự khác biệt của chúng ở trang web encode online này:

http://kjur.github.io/jsjws/tool_b64uenc.html

Chúng ta có thể tự triển khai 1 hàm encode base64url do chính mình tạo ra. Dưới đây là code mô phỏng bằng Javascript:

function base64url(source) {
  // Encode in classical base64
  encodedSource = CryptoJS.enc.Base64.stringify(source);

  // Remove padding equal characters
  encodedSource = encodedSource.replace(/=+$/, '');

  // Replace characters according to base64url specifications
  encodedSource = encodedSource.replace(/\+/g, '-');
  encodedSource = encodedSource.replace(/\//g, '_');

  return encodedSource;
}

Ở đoạn code trên mình đã sử dụng thư viện CryptoJS để có thể mã hóa base64 rồi sau đó loại bỏ các ký tự = và thay thế các ký tự + / đi.

Để có thể sử dụng được hàm trên, đầu vào của bạn cần là 1 mảng byte ở định dạng UTF-8. Ta có thể chuyển đổi từ xâu ký tự sang mảng byte bằng 1 hàm khác cũng được cung cấp bởi CryptoJS:

var source = "Hello!";

// 48 65 6c 6c 6f 21
console.log(CryptoJS.enc.Utf8.parse(source).toString());

Cuối cùng ta đã thu được phần đầu tiên của JWT:

var header = {
  "alg": "HS256",
  "typ": "JWT"
};

var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);

Payload (Claims):

Phần thứ 2 của token đó là Payload, nơi chứa các nội dung của thông tin (claim). Thông tin truyền đi có thể là mô tả của 1 thực thể (ví dụ như người dùng) hoặc cũng có thể là các thông tin bổ sung thêm cho phần Header. Nhìn chung, chúng được chia làm 3 loại: reserved, public và private.

  1. Reserved: là những thông tin đã được quy định ở trong IANA JSON Web Token Claims registry. Chúng bao gồm: Chú ý rằng các khóa của claim đều chỉ dài 3 ký tự vì mục đích giảm kích thước của Token

    • iss (issuer): tổ chức phát hành token
    • sub (subject): chủ đề của token
    • aud (audience): đối tượng sử dụng token
    • exp(expired time): thời điểm token sẽ hết hạn
    • nbf (not before time): token sẽ chưa hợp lệ trước thời điểm này
    • iat (issued at): thời điểm token được phát hành, tính theo UNIX time
    • jti: JWT ID
  2. Public: Khóa nên được quy định ở trong IANA JSON Web Token Registry hoặc là 1 URI có chứa không gian tên không bị trùng lặp.

    Ví dụ:

    “https://www.techmaster.vn/jwt_claims/is_admin”: true
    
  3. Private: Phần thông tin thêm dùng để truyền qua giữa các máy thành viên.

    Ví dụ:

    {
      "sub": "1234567890",
      "name": "paduvi",
      "admin": true
    }
    

Ví dụ cho phần Payload:

{
  “iss”: “techmaster”,
  “exp”: 1426420800,
  “https://www.techmaster.vn/jwt_claims/is_admin”: true,
  “user”: “paduvi”,
  “awesome”: true
}

Mã hóa base64url ta thu được phần thứ 2 của token:

eyJpc3MiOiJ0ZWNobWFzdGVyIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwczovL3d3dy50ZWNobWFzdGVyLnZuL2p3dF9jbGFpbXMvaXNfYWRtaW4iOnRydWUsInVzZXIiOiJwYWR1dmkiLCJhd2Vzb21lIjp0cnVlfQ

Signature:

Phần chữ ký được tạo bằng cách kết hợp 2 phần Header + Payload, rồi mã hóa nó lại bằng 1 giải thuật encode nào đó, càng phức tạp thì càng tốt, ví dụ như HMAC SHA-256

$encodedContent = base64UrlEncode(header) + “.” + base64UrlEncode(payload);
$signature = hashHmacSHA256($encodedContent);

Rồi ta sẽ thu được phần cuối của token:

uL7nEjM7ihbQe7l01rmQCtGYoKyb4VyabWqX8PZKdt4

Putting All Together:

Tổng kết lại, JWT gom lại từ ví dụ trên sẽ có dạng là:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZWNobWFzdGVyIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwczovL3d3dy50ZWNobWFzdGVyLnZuL2p3dF9jbGFpbXMvaXNfYWRtaW4iOnRydWUsInVzZXIiOiJwYWR1dmkiLCJhd2Vzb21lIjp0cnVlfQ.uL7nEjM7ihbQe7l01rmQCtGYoKyb4VyabWqX8PZKdt4

Và đây là đoạn code Javascript triển khai toàn bộ công việc trên:

var header = {
  "alg": "HS256",
  "typ": "JWT"
};

var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
var encodedHeader = base64url(stringifiedHeader);

var data = {
  "iss": "techmaster",
  "exp": 1426420800,
  "https://www.techmaster.vn/jwt_claims/is_admin": true,
  "user": "paduvi",
  "awesome": true
};

var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
var encodedData = base64url(stringifiedData);

var token = encodedHeader + "." + encodedData;

var secret = "My very confidential secret!";

var signature = CryptoJS.HmacSHA256(token, secret);
signature = base64url(signature);

var signedToken = token + "." + signature;

Mình chỉ minh họa như vậy thôi, chứ không khuyến khích mọi người tự mất công làm lại tất cả các công đoạn vì hiện nay đã có rất nhiều thư viện hỗ trợ công việc này. Các bạn có thể tham khảo danh sách các thư viện và thử debug JWT  ở trên trang web https://jwt.io/.

Hiện tại mình đang lập trình Node.js và Golang nên đề xuất 2 thư viện rất dễ sử dụng, đó là: jsonwebtoken (Node.js) và dgrijalva/jwt-go (Golang)

Ví dụ đơn giản về Authentication bằng JWT

Code bên phía API Server, sử dụng Golang:

package main

import (
	"github.com/dgrijalva/jwt-go"
	jwtmiddleware "github.com/iris-contrib/middleware/jwt"
	"github.com/kataras/iris"
)

func main() {

	myJwtMiddleware := jwtmiddleware.New(jwtmiddleware.Config{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return []byte("My Secret"), nil
		},
		SigningMethod: jwt.SigningMethodHS256,
	})

	iris.Get("/secured/ping", myJwtMiddleware.Serve, SecuredPingHandler)
	iris.Listen(":8080")

}

type Response struct {
	Text string `json:"text"`
}

func SecuredPingHandler(ctx *iris.Context) {
	response := Response{"All good. You only get this message if you're authenticated"}
	// get the *jwt.Token which contains user information using:
	// user:= myJwtMiddleware.Get(ctx) or context.Get("jwt").(*jwt.Token)
	ctx.JSON(iris.StatusOK, response)
}

Bây giờ nếu mình vào thử đường dẫn http://localhost:8080/secured/ping bằng trình duyệt ta sẽ thu được kết quả là:

Required authorization token not found

Đúng như dự kiến, truy cập vào đường dẫn không thành công do mình chưa khai báo token. Để có thể authenticate thành công, ta cần bổ sung thêm HTTP Header cho request phía client truy cập. Phía dưới là code minh họa bằng Node.js:

/**
 * Created by phanducviet on 7/11/16.
 */
const
    url = 'http://localhost:8080/secured/ping',
    request = require('request'),
    jwt = require('jsonwebtoken'),
    payload = {
        user: 'paduvi',
        company: 'Techmaster'
    },
    secretKey = 'My Secret';

var token = jwt.sign(payload, secretKey, {algorithm: 'HS256', expiresIn: '1h'});

var callback = function (error, response, body) {
    if (error) {
        console.error(error);
    } else {
        console.log("Status Code: " + response.statusCode);
        console.log("Response Data: " + body);
    }
}

var options = {
    url: url,
    headers: {
        'Authorization': 'Bearer ' + token
    }
}

request(options, callback);
// or
request.get(url, {
    'auth': {
        'bearer': token
    }
}, callback);

Kết quả hiển thị trên console là:

Status Code: 200
Response Data: {"text":"All good. You only get this message if you're authenticated"}

Nếu như mình dùng secret key không hợp lệ, kết quả trả về sẽ là:

Status Code: 401
Response Data: signature is invalid

Hay bổ sung thêm Reserved Claim nbf (Not before time) với thời gian là khoảng vài phút sau thì kết quả là:

Status Code: 401
Response Data: Token is not valid yet

Ngoài ra, các bạn có thể chỉnh sửa lại code để test nốt với các trường hợp còn lại có thể xảy ra, ví dụ như exp (expired at), iat (issued at)…

Tham khảo khóa học Web Frontend cho người mới bắt đầu - tại đây