Xác minh 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 16 trên 24.
Một client API gửi token trong header ở mỗi request: Authorization: Bearer <token>. Đọc token bằng decodeJwt sẽ chấp nhận bất kỳ token nào mà bất kỳ ai nhập vào. jwtVerify(token, key) tính lại chữ ký bằng secret của bạn và phát sinh lỗi nếu chữ ký không khớp, token đã hết hạn hoặc token không phải JWT. Bạn chỉ có thể tin cậy payload sau khi hàm này trả về.
async function requireDevice(req, res, next) {
const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
try {
const { payload } = await jwtVerify(token, deviceKey);
req.device = payload.sub;
} catch {
return res.status(401).json({ error: 'Unknown device' });
}
next();
}Thử thách
DễBây giờ hãy chấp nhận các token mà bạn ký. Ứng dụng gửi token trong header ở mỗi yêu cầu, còn máy chủ kiểm tra chữ ký trước khi tin bất cứ nội dung nào bên trong.
- Trong
server.js, nhậpjwtVerifytừjose. - Viết middleware
requireUser(req, res, next). Middleware này đọc headerAuthorization. Header đó chứa từBearer, một dấu cách, rồi đến token. Lấy token ra khỏi header. - Kiểm tra token bằng
jwtVerify, sử dụng cùng khóa mà bạn dùng để ký.jwtVerifysẽ báo lỗi khi chữ ký không khớp, khi token hết hạn hoặc khi văn bản hoàn toàn không phải token. Trong tất cả các trường hợp đó, cũng như khi không có header, hãy trả về trạng thái 401 và{"error": "Log in first"}. - Khi token hợp lệ, gán cho
req.usermột đối tượng cóid, lấy từ claimsubcủa token, vàusername, lấy từ claimusernamecủa token. Sau đó gọinext(). - Thêm
GET /mevớirequireUser. Endpoint này trả vềreq.user:{"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.
Thử nhé: Nhấn Chạy. Gửi GET /me: trạng thái 401 và {"error": "Log in first"}. Gửi POST /signup, sau đó gửi POST /token, cả hai đều dùng {"username": "ada", "password": "correct horse"}, rồi sao chép token. Trong trường Headers, thêm header có tên Authorization với giá trị gồm Bearer, một dấu cách và token của bạn. Gửi GET /me: trạng thái 200, kèm id của bạn và "username": "ada". Bây giờ, thay đổi một chữ cái ở phần giữa của token, nằm giữa hai dấu chấm, rồi gửi lại GET /me: trạng thái 401 vì chữ ký không còn khớp.
Tự làm thử
import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
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 });
});
app.post('/token', 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' });
}
const token = await new SignJWT({ username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(user._id.toHexString())
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
res.json({ token });
});
// 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ỳ claim nào 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');
});
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Xác thực và bảo mật
Tự luyện tập: Playground Express