Phiên đăng nhập
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 10 trên 24.
Đăng nhập trả về 200, nhưng yêu cầu tiếp theo lại quên mất ai đã đăng nhập. Phiên giúp ghi nhớ điều đó. Khi đăng nhập, máy chủ tạo một mã định danh không ai đoán được, lưu thông tin mã đó thuộc về người dùng nào rồi gửi mã dưới dạng cookie. Mỗi yêu cầu sau đó đều gửi lại mã này, và máy chủ tra cứu mã.
Mã định danh phải ngẫu nhiên. Nếu cookie lưu tên người dùng, bất kỳ ai cũng có thể nhập Cookie: sid=ada và trở thành Ada. randomBytes(24) tạo ra 24 byte ngẫu nhiên, tương đương 192 bit, không ai có thể đoán được. toString('base64url') chuyển chúng thành 32 chữ cái, chữ số, - và _, những ký tự an toàn trong cookie. Đặt cookie ở chế độ httpOnly để tập lệnh trên trang không thể đọc cookie.
const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });
// trong một yêu cầu sau đó
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });Thử thách
DễBước này mở dự án API ghi chú riêng tư đã chuẩn bị sẵn cho khóa học, thay vì các tệp dự án của bạn từ Ghi nhớ bằng cookie (bao gồm server.js). Phần bạn đã làm trước đó vẫn nằm trong bài học riêng. Đọc các tệp được cung cấp trước khi chỉnh sửa server.js. Bản sao này chứa các route đăng ký và đăng nhập cho ghi chú riêng tư.
Đăng nhập hoạt động, nhưng yêu cầu tiếp theo đã quên mất ai vừa đăng nhập. Hãy giúp máy chủ ghi nhớ bằng một phiên (session): một id ngẫu nhiên được lưu trên máy chủ và gửi đến trình duyệt dưới dạng cookie.
- Trong
server.js, nhậprandomBytestừnode:crypto. Nhậpsessionstừ./db.jscùng vớinotesvàusers. Tệpdb.jsbị khóa đã có bộ sưu tậpsessions. - Trong
POST /login, sau khi kiểm tra mật khẩu, hãy tạo id phiên: 24 byte ngẫu nhiên được viết dưới dạng văn bản base64url. Id này có 32 ký tự gồm chữ cái, chữ số,-và_, chẳng hạnoqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Tạo id mới cho mỗi lần đăng nhập. - Lưu phiên vào
sessionsdưới dạng tài liệu có hai trường:_id: id phiên.userId:_idcủa người dùng đã đăng nhập.
- Gửi id đến trình duyệt dưới dạng cookie có tên
sid, kèm tùy chọnhttpOnlyđể các tập lệnh trên trang không thể đọc cookie này. Phản hồi đăng nhập vẫn là{"username": "ada"}. - Thêm
GET /me. Route này đọc cookiesid, tìm phiên có_idđó, rồi tìm người dùng của phiên. Route trả về{"username": "ada"}. Khi không có cookiesid, hoặc không có phiên nào mang id đó, route trả về trạng thái 401 và{"error": "Log in first"}. Ai đó tự nhập cookie, chẳng hạnsid=ada, cũng phải nhận trạng thái 401 đó.
Thử nhé: Nhấn Chạy. Gửi POST /signup với {"username": "ada", "password": "correct horse"}. Gửi GET /me: trạng thái 401 và {"error": "Log in first"}. Gửi POST /login với cùng nội dung như khi đăng ký: trạng thái 200. Tab Headers trong phản hồi hiển thị Set-Cookie: sid=...; Path=/; HttpOnly, trong đó id ngẫu nhiên của bạn thay cho các dấu chấm. Gửi lại GET /me. Trình duyệt gửi lại cookie, nên bạn nhận được {"username": "ada"}. Mở Data: sessions có một tài liệu, với id của bạn làm _id và _id của ada làm userId.
Tự làm thử
import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, 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 });
});
// Đếm số lần trình duyệt này gọi GET /visit.
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// Ghi chú. Hiện tại, ai cũng có thể đọc và viết ghi chú.
app.get('/notes', async (req, res) => {
res.json(await notes.find().toArray());
});
app.post('/notes', async (req, res) => {
const note = { text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
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