Giới thiệu
JSON Web Token (JWT) là một phương pháp an toàn để truyền tải thông tin giữa các bên. Nó thường được sử dụng trong xác thực API, nơi mà thông tin người dùng được mã hóa và gửi đi dưới dạng token. Trong bài viết này, chúng ta sẽ tìm hiểu hai phương pháp xác thực JWT: Khóa Bí mật + JWT (phương pháp đối xứng) và Khóa Công khai + Khóa Riêng (phương pháp bất đối xứng). Chúng ta cũng sẽ xem xét các thực tiễn tốt nhất, những cạm bẫy phổ biến và một số mẹo hiệu suất hữu ích.
📋 Mục lục
- Tổng quan
- Phương pháp 1: Khóa Bí mật + JWT
- Phương pháp 2: Khóa Công khai + Khóa Riêng
- So sánh
- Các thực tiễn tốt nhất
- Khắc phục sự cố
Tổng quan
JWT là gì?
JWT là một chuỗi ký tự được mã hóa, bao gồm ba phần:
- Header: Chứa thông tin về thuật toán mã hóa và loại token.
- Payload: Chứa các thông tin (claims) về người dùng.
- Signature: Dùng để xác thực token.
Luồng xác thực
Cả hai phương pháp đều tuân theo luồng cơ bản sau:
- Khách hàng nhận một token JWT.
- Khách hàng gửi token trong các yêu cầu API.
- Máy chủ xác thực token và xử lý yêu cầu.
Phương pháp 1: Khóa Bí mật + JWT
🔍 Khi nào sử dụng
- Môi trường ứng dụng đơn lẻ.
- Tạo token tập trung (cùng máy chủ tạo và xác thực).
- Tình huống triển khai đơn giản.
- API nội bộ trong cùng một tổ chức.
🛠️ Thiết lập
Bước 1: Tạo Khóa Bí mật
javascript
// Tạo một khóa bí mật an toàn
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Bước 2: Cấu hình Môi trường
plaintext
# .env file
JWT_SECRET=khóa_bí_mật_64_ký_tự_của_bạn
JWT_EXPIRES_IN=24h
JWT_ALGORITHM=HS256
Bước 3: Cấu hình Máy chủ
javascript
// authService.js (phương pháp Khóa Bí mật)
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_ALGORITHM = 'HS256';
class AuthService {
// Tạo token
generateToken(user) {
return jwt.sign(
{
sub: user.id,
username: user.username,
iat: Math.floor(Date.now() / 1000)
},
JWT_SECRET,
{
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
algorithm: JWT_ALGORITHM
}
);
}
// Xác thực token
verifyToken(token) {
return jwt.verify(token, JWT_SECRET, { algorithms: [JWT_ALGORITHM] });
}
}
🔐 Ví dụ về Tạo Token
javascript
// Ví dụ: Tạo một token JWT (phía máy chủ)
const authService = new AuthService();
const user = {
id: "user123",
username: "john_doe"
};
const token = authService.generateToken(user);
console.log("Token đã tạo:", token);
📡 Gửi Yêu cầu API
bash
# Sử dụng curl
curl -X POST http://localhost:8000/translate \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"text": "Xin chào thế giới"}'
Phương pháp 2: Khóa Công khai + Khóa Riêng
🔍 Khi nào sử dụng (Triển khai hiện tại)
- Hệ thống phân tán (microservices).
- Tích hợp bên thứ ba.
- Yêu cầu an ninh cao hơn.
- Tách biệt việc tạo token và xác thực.
- Nhiều nhà phát hành token.
🛠️ Thiết lập
Bước 1: Tạo Cặp Khóa RSA
bash
# Sử dụng trình tạo khóa đã cung cấp
npm run generate-keys
# Hoặc thủ công với OpenSSL
openssl genrsa -out jwt-private.key 2048
openssl rsa -in jwt-private.key -pubout -out jwt-public.key
Bước 2: Cấu hình Môi trường
plaintext
# .env file
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"
JWT_ALGORITHM=RS256
Bước 3: Cấu hình Máy chủ (Hiện tại)
javascript
// authService.js (phương pháp Khóa Công khai)
import jwt from 'jsonwebtoken';
const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;
const JWT_ALGORITHM = process.env.JWT_ALGORITHM || 'RS256';
class AuthService {
constructor() {
this.jwtPublicKey = this.loadPublicKey();
}
loadPublicKey() {
if (process.env.JWT_PUBLIC_KEY) {
return process.env.JWT_PUBLIC_KEY.replace(/\\n/g, '\n');
}
console.warn('⚠️ Cảnh báo: Không tìm thấy khóa công khai JWT!');
return null;
}
// Chỉ xác thực token (không tạo trên máy chủ)
verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, this.jwtPublicKey, { algorithms: [this.jwtAlgorithm] }, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}
}
🔐 Tạo Token (Bên ngoài)
Lưu ý: Với phương pháp khóa công khai/khóa riêng, các token được tạo bên ngoài bằng cách sử dụng khóa riêng.
Sử dụng Node.js
javascript
// external-token-generator.js
import jwt from 'jsonwebtoken';
import fs from 'fs';
const privateKey = fs.readFileSync('./jwt-private.key', 'utf8');
function generateToken(user) {
const payload = {
sub: user.id,
username: user.username,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // 24 giờ
};
return jwt.sign(payload, privateKey, { algorithm: 'RS256' });
}
// Ví dụ sử dụng
const user = { id: "user123", username: "john_doe" };
const token = generateToken(user);
console.log("Token đã tạo:", token);
Sử dụng Python
python
# external_token_generator.py
import jwt
import time
from cryptography.hazmat.primitives import serialization
# Tải khóa riêng
with open('./jwt-private.key', 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None
)
def generate_token(user_id, username):
payload = {
'sub': user_id,
'username': username,
'iat': int(time.time()),
'exp': int(time.time()) + (24 * 60 * 60) # 24 giờ
}
return jwt.encode(payload, private_key, algorithm='RS256')
# Ví dụ sử dụng
token = generate_token("user123", "john_doe")
print(f"Token đã tạo: {token}")
📡 Gửi Yêu cầu API
bash
# Kiểm tra trạng thái xác thực
curl http://localhost:8000/auth/status
# Xác thực token của bạn
curl -X GET http://localhost:8000/auth/verify \
-H "Authorization: Bearer <token>"
# Thực hiện yêu cầu dịch thuật
curl -X POST http://localhost:8000/translate \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"text": "Xin chào thế giới"}'
🧪 Kiểm tra
bash
# Chạy bộ kiểm tra với token của bạn
JWT_TOKEN="token_của_bạn" npm run test-auth
# Kiểm tra client
JWT_TOKEN="token_của_bạn" node jwt-client.js
So sánh
| Tính năng | Khóa Bí mật + JWT | Khóa Công khai + Khóa Riêng |
|---|---|---|
| An ninh | Đối xứng (bí mật chung) | Bất đối xứng (khóa riêng) |
| Tạo Token | Phía máy chủ | Bên ngoài/Phía client |
| Quản lý Khóa | Một khóa bí mật | Khóa công khai trên máy chủ, khóa riêng bảo mật |
| Khả năng mở rộng | Giới hạn ở một dịch vụ | Thân thiện với hệ thống phân tán |
| Độ phức tạp | Đơn giản | Phức tạp hơn |
| Trường hợp sử dụng | API nội bộ | Tích hợp bên thứ ba |
| Thuật toán | HS256 | RS256 |
Các cân nhắc về an ninh
Phương pháp Khóa Bí mật
- ✅ Thực hiện đơn giản
- ✅ Xác thực nhanh
- ❌ Rủi ro bí mật chung
- ❌ Độ phức tạp trong quay vòng khóa
Phương pháp Khóa Công khai/Khóa Riêng
- ✅ Không có bí mật chung
- ✅ Dễ dàng quay vòng khóa
- ✅ Tạo token phân tán
- ❌ Xác thực hơi chậm hơn
- ❌ Thiết lập phức tạp hơn
Các thực tiễn tốt nhất
🔐 An ninh
- Không bao giờ commit khóa riêng vào hệ thống quản lý phiên bản.
- Sử dụng biến môi trường cho dữ liệu nhạy cảm.
- Thực hiện thời gian hết hạn token (thời gian hợp lý).
- Xác thực tất cả các claims JWT (exp, iat, sub, v.v.).
- Sử dụng HTTPS trong môi trường sản xuất.
- Thực hiện giới hạn tần suất cho các điểm cuối xác thực.
🗂️ Quản lý Khóa
bash
# Thêm vào .gitignore
echo "jwt-private.key" >> .gitignore
echo ".env" >> .gitignore
# Đặt quyền tệp phù hợp
chmod 600 jwt-private.key # Khóa riêng - chỉ chủ sở hữu đọc/ghi
chmod 644 jwt-public.key # Khóa công khai - có thể đọc bởi người khác
🏗️ Cấu trúc Token
Cấu trúc payload JWT được khuyến nghị:
json
{
"sub": "định_danh_người_dùng_unqiue",
"username": "tên_dễ_đọc",
"iat": 1642000000,
"exp": 1642086400,
"roles": ["user", "translator"],
"permissions": ["read", "translate"]
}
🔄 Làm mới Token
javascript
// Kiểm tra thời gian hết hạn token trước khi thực hiện yêu cầu
function isTokenExpired(token) {
try {
const decoded = jwt.decode(token);
return decoded.exp < Date.now() / 1000;
} catch (error) {
return true;
}
}
// Logic tự động làm mới
if (isTokenExpired(currentToken)) {
currentToken = await generateNewToken();
}
Khắc phục sự cố
Các vấn đề phổ biến
1. "Không cấu hình khóa công khai JWT"
bash
# Giải pháp: Đặt khóa công khai trong môi trường
export JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
2. "Xác thực token thất bại"
bash
# Kiểm tra sự không tương thích thuật toán
# Đảm bảo token của bạn được ký bằng RS256 cho phương pháp khóa công khai/khóa riêng
# Đảm bảo token của bạn được ký bằng HS256 cho phương pháp khóa bí mật
3. "Token đã hết hạn"
javascript
// Kiểm tra thời gian hết hạn token
const decoded = jwt.decode(token);
console.log('Token hết hạn vào lúc:', new Date(decoded.exp * 1000));
4. "Chữ ký không hợp lệ"
bash
# Đảm bảo bạn đang sử dụng khóa riêng đúng cho việc ký
# Xác nhận khóa công khai khớp với khóa riêng đã sử dụng để ký
openssl rsa -in jwt-private.key -pubout | diff - jwt-public.key
Chế độ Gỡ lỗi
bash
# Bật ghi log gỡ lỗi
DEBUG=jwt* npm start
# Kiểm tra chi tiết token mà không cần xác thực
node -e "console.log(JSON.stringify(require('jsonwebtoken').decode('token_của_bạn'), null, 2))"
Kiểm tra Các Điểm cuối
bash
# Kiểm tra sức khỏe (không yêu cầu xác thực)
curl http://localhost:8000/health
# Trạng thái xác thực
curl http://localhost:8000/auth/status
# Xác thực token
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/auth/verify
🎯 Bước tiếp theo
- Chọn phương pháp dựa trên yêu cầu của bạn.
- Tạo các khóa phù hợp (khóa bí mật hoặc cặp RSA).
- Cấu hình biến môi trường.
- Kiểm tra việc tạo và xác thực token.
- Triển khai trong ứng dụng của bạn.
- Thiết lập giám sát và ghi log.
Danh sách kiểm tra sản xuất
- Khóa được lưu trữ an toàn (biến môi trường, dịch vụ quản lý khóa).
- Khóa riêng không bao giờ trên máy chủ (phương pháp khóa công khai/khóa riêng).
- HTTPS được bật.
- Thời gian hết hạn của token được cấu hình.
- Giới hạn tần suất được thực hiện.
- Ghi log và giám sát được thiết lập.
- Xử lý lỗi được thực hiện.
- Tài liệu được cập nhật.
Bài viết này đề cập đến cả hai phương pháp xác thực JWT. Hãy chọn phương pháp phù hợp nhất với kiến trúc và yêu cầu an ninh của bạn.