Xây dựng hệ thống đăng nhập an toàn cho Node.js: Hướng dẫn chi tiết và các chiến lược bảo mật hàng đầu

0 0 0

Người đăng: Gung Typical

Theo Viblo Asia

Bài viết này giới thiệu các chiến lược đăng nhập an toàn cho Node.js, triển khai nhiều kỹ thuật bảo mật để bảo vệ thông tin đăng nhập, phiên làm việc và quyền truy cập tài khoản của người dùng. Cùng tìm hiểu các tính năng bảo mật quan trọng và cách triển khai chúng hiệu quả.

Các tính năng bảo mật chính

1. Băm mật khẩu với Salt

Mật khẩu được băm bằng thuật toán băm mạnh (ví dụ: bcrypt) cùng với một salt ngẫu nhiên cho mỗi mật khẩu. Điều này đảm bảo rằng ngay cả khi mật khẩu băm bị xâm phạm, nó cũng không thể dễ dàng bị đảo ngược thành mật khẩu gốc.

Tại sao điều này lại quan trọng:

  • Ngăn chặn việc lưu trữ mật khẩu dưới dạng văn bản thuần.
  • Việc sử dụng salt khiến kẻ tấn công khó sử dụng các bảng băm được tính toán trước (rainbow tables) để đoán mật khẩu.

VD:

import bcrypt from 'bcrypt'; const hashPassword = async (password: string): Promise<string> => { const saltRounds = 10; const salt = await bcrypt.genSalt(saltRounds); return await bcrypt.hash(password, salt);
}; // Create and save the user in the database
const newUser = new User({ username, password: hashPassword });
await newUser.save(); // Login API
const { username, password } = req.body; const user = users.find((u) => u.username === username);
if (!user) return res.status(400).send('User not found'); const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) return res.status(400).send('Incorrect password');

2. Xác thực dựa trên Token

JWT (JSON Web Token) được sử dụng để xác thực dựa trên token, cho phép xác thực không trạng thái giữa máy khách và máy chủ. Người dùng nhận được JWT khi đăng nhập, sau đó được gửi kèm với mọi yêu cầu tiếp theo trong tiêu đề Authorization.

Tại sao điều này lại quan trọng:

  • Không trạng thái: Không cần lưu trữ phiên trên máy chủ.
  • Token bảo mật có thể mang các claims (ID người dùng, vai trò) mà không tiết lộ thông tin nhạy cảm.

VD:

import jwt from 'jsonwebtoken'; const generateToken = (userId: string) => { return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '1h' });
};

3. Refresh Token

Refresh token được sử dụng để lấy access token mới mà không yêu cầu người dùng phải đăng nhập lại. Điều này đảm bảo trải nghiệm mượt mà cho người dùng mà không ảnh hưởng đến bảo mật.

Tại sao điều này lại quan trọng:

  • Giảm thiểu rủi ro bằng cách sử dụng access token tồn tại trong thời gian ngắn.
  • Refresh token được lưu trữ an toàn hơn (ví dụ: trong cơ sở dữ liệu hoặc HTTP-only cookie).

VD:

const generateRefreshToken = (userId: string) => { return jwt.sign({ userId }, process.env.REFRESH_SECRET, { expiresIn: '7d' });
};

4. HTTP-Only Cookies

Lưu trữ các token nhạy cảm như refresh token trong HTTP-only cookies. Điều này ngăn JavaScript truy cập các token, bảo vệ chúng khỏi các cuộc tấn công XSS (Cross-Site Scripting).

Tại sao điều này lại quan trọng:

  • Bảo vệ token khỏi bị đánh cắp bởi các đoạn mã độc hại.
  • Giảm nguy cơ lộ token thông qua mã phía máy khách.

VD:

res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict',
});

5. Danh sách đen JWT (JWT Blacklisting)

Các token JWT không có cơ chế thu hồi tích hợp. Để vô hiệu hóa token (ví dụ: khi đăng xuất), JWT blacklisting được sử dụng. Cơ sở dữ liệu hoặc Redis được sử dụng để lưu trữ các token không hợp lệ cho đến khi chúng hết hạn.

Tại sao điều này lại quan trọng:

  • Ngăn chặn các token đã được cấp trước đó bị sử dụng sau khi đăng xuất hoặc tài khoản bị xâm phạm.

VD:

const blacklistToken = async (token: string) => { await redisClient.set(token, 'blacklisted', 'EX', tokenExpirationTime);
};

6. Chỉ một phiên hoạt động tại một thời điểm

Đảm bảo rằng chỉ có một phiên hoạt động cho một người dùng tại bất kỳ thời điểm nào. Khi một phiên mới được tạo, hãy vô hiệu hóa các token hoặc phiên trước đó.

Tại sao điều này lại quan trọng:

  • Ngăn chặn việc đăng nhập đồng thời từ nhiều thiết bị hoặc vị trí, tăng cường bảo mật tài khoản.

VD:

// Schema for User table:
sessionId: { type: String, unique: true } //login API
const newSessionId = generateAccessToken(user);
user.sessionId = newSessionId;
await user.save(); // Auth API
const decodedToken: any = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!); // Replace with your JWT secret key
const sessionIdFromToken = decodedToken.sessionId; if (user.sessionId !== sessionIdFromToken) { return res.status(403).json({ message: 'Session invalidated. You are logged in elsewhere.' }); }

7. Thời gian hết hạn phiên lý tưởng

Phiên hết hạn sau một khoảng thời gian không hoạt động. Triển khai bộ hẹn giờ hết hạn phiên để tự động đăng xuất người dùng không hoạt động trong một khoảng thời gian xác định trước.

Tại sao điều này lại quan trọng:

  • Giảm nguy cơ người không được phép truy cập vào phiên nếu người dùng quên đăng xuất.

VD:

const sessionExpirationTime = 15 * 60 * 1000; // 15 minutes
const IDLE_TIMEOUT = 15 * 60 * 1000; // User Schema
lastActivity: { type: Date, default: Date.now }, // Track last activity timestamp // in auth middleware
const now = new Date();
const idleTime = now - new Date(user.lastActivity); if (idleTime > IDLE_TIMEOUT) { // Token is still valid, but session is idle for too long return res.status(401).json({ message: 'Session expired due to inactivity' });
} // Else Update last activity timestamp if within idle timeout
user.lastActivity = now;
await user.save();

8. Cơ chế khóa tài khoản

Ngăn chặn các cuộc tấn công brute force bằng cách khóa tài khoản sau một số lần đăng nhập thất bại. Triển khai khóa dựa trên thời gian, trong đó tài khoản bị khóa tạm thời sau nhiều lần thất bại liên tiếp.

Tại sao điều này lại quan trọng:

  • Giảm thiểu các cuộc tấn công brute force bằng cách giới hạn số lần đăng nhập.

VD:

// Schema
failedLoginAttempts: { type: Number, default: 0 },
lockoutUntil: { type: Date, default: null }, // Track lockout time const MAX_ATTEMPTS = 3;
const LOCK_TIME = 15 * 60 \_ 1000; // 15 minutes
// Example for MongoDB with Mongoose
const handleFailedLogin = async (email: string) => { const user = await User.findOne({ email }); if (!user) return false; if (user.lockoutUntil && user.lockoutUntil > new Date()) { return true; // Account is still locked } user.failedLoginAttempts += 1; if (user.failedLoginAttempts >= MAX_ATTEMPTS) { user.lockoutUntil = new Date(Date.now() + LOCK_TIME); // Lock for 30 mins } await user.save(); return user.lockoutUntil && user.lockoutUntil > new Date(); // Return if account is locked
}; // Reset login attempts if the login is successful

9. Quy trình đặt lại mật khẩu an toàn

Người dùng có thể đặt lại mật khẩu của họ thông qua một quy trình an toàn. Sử dụng token dùng một lần (OTP) có giới hạn thời gian được gửi đến email của người dùng cho quy trình đặt lại. Đảm bảo token đặt lại hết hạn sau một khoảng thời gian ngắn.

Tại sao điều này lại quan trọng:

  • Cung cấp cho người dùng một cách an toàn để khôi phục tài khoản mà không ảnh hưởng đến bảo mật.

VD:

// User Schema
resetPasswordToken: String,
resetPasswordExpires: Date, /* Use crypto.randomBytes() to generate a secure hex instead of Math.random(), Math.random() hex are easier for attackers to guess or brute force. */
const generateResetToken = () => { return crypto.randomBytes(20).toString('hex');
}; const token = generateResetToken();
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now

10. Hết hạn mật khẩu và hạn chế 3 mật khẩu gần nhất

Đây là cách hoạt động:

  • Lịch sử mật khẩu:
  • Hệ thống lưu trữ ba mật khẩu băm gần nhất trong lịch sử mật khẩu của người dùng.
  • Khi người dùng cố gắng đặt mật khẩu mới, hệ thống so sánh mật khẩu mới với các mật khẩu trước đó để ngăn chặn việc sử dụng lại.
  • Hết hạn mật khẩu:
  • Hệ thống theo dõi thời gian người dùng đã sử dụng mật khẩu hiện tại của họ.
  • Sau một khoảng thời gian đặt trước (ví dụ: 1 tháng), hệ thống gửi thông báo nhắc người dùng thay đổi mật khẩu của họ.
  • Nếu mật khẩu không được cập nhật trong khoảng thời gian gia hạn nhất định, quyền truy cập có thể bị hạn chế cho đến khi mật khẩu mới được đặt.

VD:

// Schema
passwordHistory: [{ type: String }], // Array to store past 3 password hashes
passwordUpdatedAt: { type: Date, default: Date.now }, // When the password was last updated // You can Check Expiration on Login or have a corn job to find and notify users via email.

11. Triển khai chính sách mật khẩu mạnh

Chính sách mật khẩu mạnh giúp ngăn chặn truy cập trái phép và giảm thiểu nguy cơ tấn công brute-force, tấn công từ điển và nhồi tin.

VD:

import validator from 'validator'; const password = 'User@1234';
if ( !validator.isStrongPassword(password, { minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, })
) { throw new Error('Password does not meet complexity requirements.');
}

12. Thu hồi Token cụ thể theo người dùng

Thu hồi token cụ thể theo người dùng là quá trình vô hiệu hóa token của một người dùng duy nhất mà không ảnh hưởng đến những người khác.

Nó hữu ích trong các trường hợp như:

  • Phát hiện hoạt động đáng ngờ trên tài khoản
  • Người dùng yêu cầu thu hồi token vì lý do bảo mật
  • Tài khoản bị vô hiệu hóa hoặc bị tạm ngưng
  • Token bị đánh cắp hoặc bị rò rỉ

Cách thức hoạt động:

  • Cấu trúc Token: Token (ví dụ: JWT) chứa dữ liệu người dùng và tokenVersion. TokenVersion được lưu trữ trong cơ sở dữ liệu để theo dõi tính hợp lệ của token cho mỗi người dùng.

  • Xác minh Token: Khi một yêu cầu được thực hiện, máy chủ giải mã token và so sánh tokenVersion trong token với phiên bản được lưu trữ trong cơ sở dữ liệu. Nếu chúng khớp, token hợp lệ; nếu không, token bị vô hiệu hóa.

  • Thu hồi Token: Để thu hồi tất cả token cho một người dùng, chỉ cần tăng tokenVersion trong cơ sở dữ liệu. Tất cả token có phiên bản trước đó sẽ tự động bị vô hiệu hóa.

VD:

// Schema: token_version INT DEFAULT 1 // JWT Creation
const createToken = (user) => { const payload = { userId: user.id, tokenVersion: user.tokenVersion }; return jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
}; // Token Validation
if (user.tokenVersion !== decoded.tokenVersion) { throw new Error('Invalid token: version mismatch');
} // To revoke, increment tokenVersion
user.tokenVersion++;

13. Cấu hình chia sẻ tài nguyên xuất chéo (CORS)

Cấu hình CORS để hạn chế nguồn gốc nào được phép truy cập API. Chỉ các tên miền đáng tin cậy mới được phép gửi yêu cầu đến máy chủ của bạn.

Tại sao điều này lại quan trọng:

  • Ngăn chặn các trang web trái phép thực hiện yêu cầu đến máy chủ của bạn.
  • Bảo vệ chống lại các cuộc tấn công CSRF (Cross-Site Request Forgery).

VD:

import cors from 'cors'; app.use( cors({ origin: 'https://your-frontend-domain.com', credentials: true, })
);

Bình luận

Bài viết tương tự

- vừa được xem lúc

Cài đặt WSL / WSL2 trên Windows 10 để code như trên Ubuntu

Sau vài ba năm mình chuyển qua code trên Ubuntu thì thật không thể phủ nhận rằng mình đã yêu em nó. Cá nhân mình sử dụng Ubuntu để code web thì thật là tuyệt vời.

0 0 396

- vừa được xem lúc

Hướng dẫn làm bot Facebook messenger cho tài khoản cá nhân

Giới thiệu. Trong bài viết trước thì mình có hướng dẫn các bạn làm chatbot facebook messenger cho fanpage. Hôm nay mình sẽ hướng dẫn các bạn tạo chatbot cho một tài khoản facebook cá nhân. Chuẩn bị.

0 0 190

- vừa được xem lúc

Crawl website sử dụng Node.js và Puppeteer - phần 2

trong phần 1 mình đã giới thiệu về puppeteer và tạo được 1 project cùng một số file đầu tiên để các bạn có thể crawl dữ liệu từ một trang web bất kỳ. Bài này mình sẽ tiếp nối bài viết trước để hoàn thiện seri này.

0 0 73

- vừa được xem lúc

Điều React luôn giữ kín trong tim

■ Mở đầu. Ngồi viết bài khi đang nghĩ vu vơ chuyện con gà hay quả trứng có trước, mình phân vân chưa biết sẽ chọn chủ đề gì để chúng ta có thể cùng nhau bàn luận.

0 0 59

- vừa được xem lúc

Gửi Mail với Nodejs và AWS SES

AWS SES. AWS SES là gì.

0 0 83

- vừa được xem lúc

Crawl website sử dụng Node.js và Puppeteer - phần 1

Bài viết này mình sẽ giới thiệu cho các bạn craw dữ liệu của web site sử dụng nodejs và Puppeteer. .

0 0 164