Ký token
Thuộc phần Xác thực và bảo mật trong hành trình Node.js của Coddy. Bài học 15 trên 24.
Một token chỉ có giá trị nếu không ai khác có thể tạo ra token đó. Với HS256, jose kết hợp header và payload với một bí mật chỉ máy chủ biết, rồi băm kết quả. Mã băm đó là chữ ký. Không có bí mật này, không ai có thể tạo ra chữ ký khớp.
Bí mật này lấy từ biến môi trường, process.env.JWT_SECRET, không lấy từ code: code được chia sẻ, sao chép và commit. Bài học này thiết lập JWT_SECRET cho bạn. jose cần giá trị này ở dạng byte.
const key = new TextEncoder().encode(process.env.INVITE_SECRET);
const invite = await new SignJWT({ room: 'design' })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(guestId)
.setIssuedAt()
.setExpirationTime('2d')
.sign(key);setSubject nhận văn bản, còn setExpirationTime nhận khoảng thời gian như '30m' hoặc '2d'.
Thử thách
DễCác ứng dụng gọi API của bạn, chẳng hạn như ứng dụng trên điện thoại, không lưu cookie như trình duyệt. Thay vào đó, hãy cấp cho chúng một token: một JWT đã ký mà chúng gửi lại cùng mỗi yêu cầu. POST /token là cách đăng nhập dành cho các ứng dụng đó.
- Trong
server.js, nhậpSignJWTtừjosecùng vớidecodeJwt. Phía trên các route, chuyểnprocess.env.JWT_SECRETthành byte bằngTextEncoder, như bài học hướng dẫn, vìjoseký bằng byte. Bài học này đã thiết lậpJWT_SECRETcho bạn. - Thêm
POST /token. Route này nhận cùng phần thân như đăng nhập,{"username": "ada", "password": "correct horse"}, rồi kiểm tra tên người dùng và mật khẩu giống nhưPOST /login. Nếu thông tin không đúng, hãy trả về trạng thái 401 cùng{"error": "Wrong username or password"}. - Nếu thông tin đúng, hãy ký một token và trả về token đó dưới dạng
{"token": "..."}. Token phải:- dùng thuật toán HS256, được ký bằng các byte của
JWT_SECRET; - chứa claim
usernametrong payload; - dùng
_idcủa người dùng dưới dạng văn bản làm subject. Hãy dùngtoHexString(), vìjosetừ chối subject không phải văn bản; - hết hạn sau khi được tạo một giờ.
Token trong bài học này có dạng như sau:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug - dùng thuật toán HS256, được ký bằng các byte của
Thử nhé: Nhấn Run. Gửi POST /signup với {"username": "ada", "password": "correct horse"}. Gửi POST /token với cùng phần thân: trạng thái 200 và {"token": "..."}. Sao chép token rồi gửi GET /inspect?token=, theo sau là token đó. Bạn sẽ thấy nội dung tương tự như {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200}: exp muộn hơn iat 3600 giây. Gửi POST /token với {"username": "ada", "password": "wrong horse"}: trạng thái 401.
Tự làm thử
import express from 'express';
import { decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
res.json({ username: user.username });
});
// Hiển thị nội dung của token: GET /inspect?token=...
app.get('/inspect', (req, res) => {
try {
res.json(decodeJwt(String(req.query.token ?? '')));
} catch {
res.status(400).json({ error: 'Not a JWT' });
}
});
// Trang quản trị. Không thể tin bất kỳ thông tin nào trong token trước khi kiểm tra chữ ký của nó.
app.get('/admin', (req, res) => {
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Tất cả bài học trong Xác thực và bảo mật
Tự luyện tập: Playground Express