Xác thực đăng nhập bằng JWT trong Node.js và Express: đăng ký, đăng nhập, route được bảo vệ

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

API công khai ai cũng gọi được thì khó mà dùng cho dữ liệu cá nhân. Bài này hướng dẫn bạn làm cơ chế xác thực bằng JWT (JSON Web Token) trong Express: đăng ký, đăng nhập và bảo vệ route, đồng thời lưu mật khẩu đúng cách bằng băm bcrypt.

Luồng xác thực bằng JWT

Luồng cơ bản gồm ba bước. Người dùng đăng nhập bằng email và mật khẩu. Máy chủ kiểm tra, nếu đúng thì ký và trả về một token. Với các yêu cầu sau, client gửi token trong header Authorization: Bearer <token>, máy chủ kiểm tra chữ ký để biết người gọi là ai mà không cần lưu phiên.

JWT gồm ba phần (header, payload, chữ ký) mã hóa dạng base64. Payload chỉ được ký chứ không bị mã hóa, nên bất kỳ ai cũng đọc được nội dung. Vì vậy tuyệt đối không đặt mật khẩu hay thông tin nhạy cảm vào trong token.

Bước 1: Khởi tạo dự án

mkdir jwt-demo
cd jwt-demo
npm init -y
npm install express bcryptjs jsonwebtoken dotenv

Tạo file .env (và thêm vào .gitignore):

JWT_SECRET=thay_bang_chuoi_bi_mat_dai_va_ngau_nhien
PORT=3000

Khóa bí mật phải dài, ngẫu nhiên, và không bao giờ đưa lên kho mã. Ai có khóa này có thể tự tạo token giả. Bạn có thể sinh khóa bằng node -e "console.log(require('crypto').randomBytes(48).toString('hex'))".

Bước 2: Đăng ký và đăng nhập

Để tập trung vào xác thực, ta lưu người dùng trong mảng ở bộ nhớ; dự án thật sẽ dùng cơ sở dữ liệu. Tạo index.js:

require('dotenv').config();
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

const users = []; // { id, email, passwordHash }
let nextId = 1;

function signToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

app.post('/auth/register', async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password || password.length < 8) {
    return res.status(400).json({ error: 'Email và mật khẩu (tối thiểu 8 ký tự) là bắt buộc' });
  }
  if (users.some((u) => u.email === email)) {
    return res.status(409).json({ error: 'Email đã được đăng ký' });
  }

  const passwordHash = await bcrypt.hash(password, 10);
  const user = { id: nextId++, email, passwordHash };
  users.push(user);

  res.status(201).json({ id: user.id, email: user.email });
});

app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users.find((u) => u.email === email);

  // Thông báo chung để không lộ email nào đã tồn tại
  const ok = user && (await bcrypt.compare(password || '', user.passwordHash));
  if (!ok) {
    return res.status(401).json({ error: 'Email hoặc mật khẩu không đúng' });
  }

  res.json({ token: signToken(user) });
});

Điểm cốt lõi: ta không bao giờ lưu mật khẩu gốc, chỉ lưu bản băm bằng bcrypt.hash. Bcrypt tự thêm "muối" ngẫu nhiên và được thiết kế chậm có chủ đích để khó dò mật khẩu bằng cách thử hàng loạt. Khi đăng nhập, bcrypt.compare so sánh mật khẩu nhập vào với bản băm. Trả thông báo lỗi giống nhau cho cả trường hợp sai email lẫn sai mật khẩu để kẻ xấu không dò được email nào có tồn tại.

Bước 3: Middleware bảo vệ route

function requireAuth(req, res, next) {
  const header = req.headers.authorization || '';
  const [scheme, token] = header.split(' ');

  if (scheme !== 'Bearer' || !token) {
    return res.status(401).json({ error: 'Thiếu token' });
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = { id: payload.sub, email: payload.email };
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Token không hợp lệ hoặc đã hết hạn' });
  }
}

app.get('/me', requireAuth, (req, res) => {
  res.json({ message: 'Xin chào!', user: req.user });
});

app.listen(process.env.PORT || 3000, () => {
  console.log('Server đang chạy');
});

Middleware requireAuth chạy trước handler. Nếu token đúng chữ ký và còn hạn, nó gắn thông tin người dùng vào req.user rồi gọi next(); ngược lại trả 401. Ta chỉ cần thêm requireAuth vào bất kỳ route nào cần bảo vệ.

Bước 4: Thử bằng curl

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"an@example.com","password":"matkhau12345"}'

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"an@example.com","password":"matkhau12345"}'

curl http://localhost:3000/me -H "Authorization: Bearer DÁN_TOKEN_VÀO_ĐÂY"

Lưu ý bảo mật quan trọng

  • Luôn dùng HTTPS ở môi trường thật, vì token gửi qua HTTP thường có thể bị đánh cắp.
  • Đặt thời hạn ngắn cho token truy cập và dùng cơ chế refresh token nếu cần phiên dài.
  • Nơi lưu token ở client: lưu trong localStorage dễ bị XSS đánh cắp, cookie HttpOnly an toàn hơn trước XSS nhưng cần chống CSRF. Mỗi lựa chọn có đánh đổi riêng.
  • Giới hạn số lần thử đăng nhập (rate limiting) để chống dò mật khẩu.
  • Chỉ định thuật toán khi xác minh nếu cấu hình nâng cao, và không bao giờ chấp nhận token không có chữ ký.
  • Đừng tự phát minh mật mã. Dùng thư viện đã được kiểm chứng và cập nhật thường xuyên.

Kết luận

Bạn đã có đủ bộ ba của xác thực cơ bản: băm mật khẩu, cấp token khi đăng nhập và middleware kiểm tra token. Bước tiếp theo là thay mảng bằng cơ sở dữ liệu, thêm phân quyền theo vai trò và thêm refresh token.

Để học backend và bảo mật API một cách bài bản, bạn có thể xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment