Làm chủ xác thực trong ứng dụng MERN Stack với JWT
Trong hướng dẫn này, chúng ta sẽ tập trung vào việc triển khai xác thực JWT trong một ứng dụng MERN stack. JWT là một phương pháp phổ biến và hiệu quả để xử lý xác thực người dùng trong các ứng dụng web hiện đại.
Sau khi hoàn thành hướng dẫn này, bạn sẽ có thể thiết lập chức năng đăng ký, đăng nhập người dùng và bảo vệ các route bằng JWT.
JWT là gì?
JWT (JSON Web Token) là một tiêu chuẩn mở (RFC 7519) định nghĩa cách thức nhỏ gọn và tự chứa để truyền thông tin một cách an toàn giữa các bên dưới dạng một đối tượng JSON. Thông tin trong JWT được ký số, đảm bảo tính toàn vẹn và xác thực của dữ liệu. Vì JWT có thể tự ký, nó thường được sử dụng trong xác thực và trao đổi thông tin giữa các ứng dụng web.
Lợi ích khi sử dụng JWT
- Xác thực không trạng thái: JWT là stateless, nghĩa là không cần lưu thông tin phiên (session) trên máy chủ.
- Bảo mật: Token JWT có thể được ký và mã hóa, đảm bảo tính bảo mật và toàn vẹn dữ liệu.
- Khả năng mở rộng: Vì là stateless nên JWT rất phù hợp cho các ứng dụng phân tán hoặc chạy trên nhiều server.
Cách JWT hoạt động
- Client đăng nhập: Người dùng cung cấp thông tin đăng nhập (username và password).
- Server xác thực thông tin: So sánh với cơ sở dữ liệu.
- Tạo JWT: Nếu đúng, server tạo ra một token chứa thông tin người dùng (ví dụ user ID).
- Client nhận token: Lưu token trong localStorage hoặc cookie.
- Xác thực token: Mỗi lần gọi API, token được gửi kèm trong Authorization header, server xác minh token.
Thiết lập dự án
Tạo một ứng dụng MERN cơ bản:
mkdir mern-jwt-auth
cd mern-jwt-auth
npm init -y
Cài đặt các thư viện cần thiết:
npm install express mongoose jsonwebtoken bcryptjs dotenv cors
npm install --save-dev nodemon
Cấu trúc thư mục phía backend:
backend/
├── config/
├── controllers/
├── middleware/
├── models/
├── routes/
└── server.js
Tạo Model người dùng
Tạo file User.js
trong thư mục models
:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
// Hash the password before saving to the database
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
const User = mongoose.model('User', userSchema);
module.exports = User;
Giải thích cấu trúc Model ở trên
-
User Schema: Tôi định nghĩa User schema bằng Mongoose, cho phép tôi mô hình hóa dữ liệu MongoDB của mình. Schema định nghĩa cấu trúc của các tài liệu tôi lưu trữ trong cơ sở dữ liệu. Ví dụ, các trường
name
,email
, vàpassword
rất quan trọng đối với quy trình xác thực của tôi. -
Email là duy nhất: Tôi đảm bảo trường
email
này là duy nhất để không có hai người dùng nào có thể đăng ký bằng cùng một địa chỉ email. Điều này rất quan trọng để ngăn ngừa xung đột trong quá trình xác thực và đảm bảo mỗi người dùng được xác định bằng một email duy nhất. -
Hash mật khẩu: Tôi sử dụng bcryptjs để băm (hash) mật khẩu của người dùng trước khi lưu vào cơ sở dữ liệu. Điều này được thực hiện trong hook
pre('save')
, được thực thi trước khi tài liệu người dùng được lưu vào cơ sở dữ liệu. Tại sao phải băm? Lưu trữ mật khẩu dưới dạng văn bản thuần túy rất không an toàn. Băm đảm bảo rằng ngay cả khi cơ sở dữ liệu bị xâm phạm, mật khẩu thực tế vẫn không thể đọc được. -
Tại sao
this.isModified('password')?
: Kiểm tra này đảm bảo rằng tôi chỉ băm mật khẩu khi mật khẩu mới được tạo hoặc cập nhật, tránh việc băm không cần thiết nếu mật khẩu chưa thay đổi.
Nếu bạn không quen thuộc với mongoose schema, bạn có thể kiểm tra tài liệu chính thức của nó: https://mongoosejs.com/docs/guide.html
Tạo các Route đăng ký và đăng nhập
Trong thư mục routes
, tạo file authRoutes.js
:
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Register user
router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) return res.status(400).json({ msg: 'User already exists' });
const user = new User({ name, email, password });
await user.save();
res.json({ msg: 'User registered successfully' });
});
// Login user
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ msg: 'User does not exist' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ msg: 'Invalid credentials' });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
module.exports = router;
Các bước xác thực JWT
-
Register Route: Đầu tiên, tôi kiểm tra xem người dùng đã tồn tại hay chưa bằng cách sử dụng
User.findOne({ email })
. Điều này ngăn nhiều người dùng đăng ký bằng cùng một địa chỉ email. Nếu email đã tồn tại, chúng tôi sẽ trả về thông báo lỗi. -
Tuyến đăng nhập: Sau khi xác thực email, tôi sử dụng bcryptjs để so sánh mật khẩu được cung cấp với mật khẩu băm được lưu trữ trong cơ sở dữ liệu. Nếu mật khẩu không đúng, chúng tôi sẽ trả về lỗi.
-
Tạo JWT: Sau khi thông tin xác thực của người dùng được xác thực, tôi tạo mã thông báo JWT bằng cách sử dụng
jwt.sign()
. Thông tinid
của người dùng được bao gồm trong tải trọng của mã thông báo, cho phép máy chủ xác định người dùng trong các yêu cầu tiếp theo. -
Tại sao sử dụng JWT? JWT cho phép chúng ta tạo các phiên không trạng thái. Khi người dùng đăng nhập, họ sẽ nhận được một mã thông báo, có thể được lưu trữ ở phía máy khách (thường là trong bộ nhớ cục bộ hoặc cookie). Máy chủ không cần phải nhớ bất cứ điều gì về người dùng giữa các yêu cầu, khiến cách tiếp cận này có khả năng mở rộng và hiệu quả.
Middleware bảo vệ Route
Middleware đề cập đến các hàm xử lý yêu cầu trước khi đến trình xử lý tuyến đường. Các hàm này có thể sửa đổi các đối tượng yêu cầu và phản hồi, kết thúc chu kỳ yêu cầu-phản hồi hoặc gọi hàm middleware tiếp theo. Các hàm middleware được thực thi theo thứ tự chúng được xác định.
Hình ảnh bên dưới cho thấy cách phần mềm trung gian hoạt động.
Bây giờ quay lại phần xác thực của chúng ta.
Để bảo vệ một số route nhất định và đảm bảo chỉ những người dùng đã xác thực mới có thể truy cập vào chúng, hãy tạo file authMiddleware.js
trong thư mục middleware
:
const jwt = require('jsonwebtoken');
const authMiddleware = (req, res, next) => {
const token = req.header('Authorization')?.split(' ')[1];
if (!token) return res.status(401).json({ msg: 'No token, authorization denied' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(400).json({ msg: 'Token is not valid' });
}
};
module.exports = authMiddleware;
Middleware này sẽ kiểm tra token JWT được gửi trong header và xác minh tính hợp lệ của nó trước khi cho phép truy cập route được bảo vệ.
Thiết lập giao diện Frontend với React
Cài đặt Axios để gửi request HTTP:
npm install axios
Tạo component Login.js
:
import React, { useState } from 'react';
import axios from 'axios';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
try {
const response = await axios.post('http://localhost:5000/api/auth/login', { email, password });
localStorage.setItem('token', response.data.token);
} catch (err) {
console.error('Error logging in', err);
}
};
return (
<div>
<input type='email' value={email} onChange={(e) => setEmail(e.target.value)} placeholder='Email' />
<input type='password' value={password} onChange={(e) => setPassword(e.target.value)} placeholder='Password' />
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
Kết luận
Xác thực bằng JWT là một phương pháp đơn giản, hiệu quả và có khả năng mở rộng để xử lý đăng nhập người dùng trong ứng dụng web hiện đại.
Qua hướng dẫn này, bạn đã biết cách triển khai xác thực bằng JWT trong ứng dụng MERN stack. Bạn có thể nâng cấp hệ thống bằng các tính năng như phân quyền người dùng, refresh token và xử lý token hết hạn.
Cảm ơn các bạn đã theo dõi!
All rights reserved