Menu
CoddyTech

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 });
challenge icon

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.

  1. Trong server.js, nhập randomBytes từ node:crypto. Nhập sessions từ ./db.js cùng với notes và users. Tệp db.js bị khóa đã có bộ sưu tập sessions.
  2. 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ạn oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Tạo id mới cho mỗi lần đăng nhập.
  3. Lưu phiên vào sessions dưới dạng tài liệu có hai trường:
    • _id: id phiên.
    • userId: _id của người dùng đã đăng nhập.
  4. Gửi id đến trình duyệt dưới dạng cookie có tên sid, kèm tùy chọn httpOnly để 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"}.
  5. Thêm GET /me. Route này đọc cookie sid, 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ó cookie sid, 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ạn sid=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