🌈个人主页:前端青山
🔥系列专栏:node.js篇
🔖人终将被年少不可得之物困其一生
依旧青山,本期给大家带来node.js篇专栏内容:node.js-增强 API 安全性和性能优化
前言
在前几篇文章中,我们已经构建了一个基本的 Express API 服务,实现了 CRUD 操作、环境变量管理、日志记录、错误处理和数据库连接优化。本文将继续在这个基础上,进一步增强 API 的安全性和性能优化。我们将添加身份验证、CORS 配置、缓存机制和更详细的性能监控。
目录
[1. 添加身份验证](#1. 添加身份验证)
[1.1 安装依赖](#1.1 安装依赖)
[1.2 创建用户模型](#1.2 创建用户模型)
[1.3 创建用户注册和登录路由](#1.3 创建用户注册和登录路由)
[1.4 添加身份验证中间件](#1.4 添加身份验证中间件)
[1.5 保护受限制的路由](#1.5 保护受限制的路由)
[2. 配置 CORS](#2. 配置 CORS)
[3. 添加缓存机制](#3. 添加缓存机制)
[3.1 创建 Redis 客户端](#3.1 创建 Redis 客户端)
[3.2 添加缓存中间件](#3.2 添加缓存中间件)
[3.3 使用缓存中间件](#3.3 使用缓存中间件)
[4. 性能监控](#4. 性能监控)
1. 添加身份验证
为了保护 API 的安全性,我们将添加 JWT(JSON Web Token)身份验证。JWT 是一种广泛使用的身份验证机制,可以确保只有经过身份验证的用户才能访问受保护的资源。
1.1 安装依赖
安装 jsonwebtoken
和 bcryptjs
库:
javascript
npm install jsonwebtoken bcryptjs
1.2 创建用户模型
在 models
目录下创建 user.js
文件:
javascript
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
const User = mongoose.model('User', userSchema);
module.exports = User;
1.3 创建用户注册和登录路由
在 routes
目录下创建 auth.js
文件:
javascript
const express = require('express');
const { celebrate, Joi } = require('celebrate');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/user');
const { SECRET_KEY } = process.env;
const router = express.Router();
const registerSchema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
});
const loginSchema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
});
router.post('/register', celebrate({ body: registerSchema }), async (req, res, next) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ username, password: hashedPassword });
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (err) {
next(err);
}
});
router.post('/login', celebrate({ body: loginSchema }), async (req, res, next) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, SECRET_KEY, { expiresIn: '1h' });
res.status(200).json({ token });
} catch (err) {
next(err);
}
});
module.exports = router;
1.4 添加身份验证中间件
在 middlewares
目录下创建 auth.js
文件:
javascript
const jwt = require('jsonwebtoken');
const { SECRET_KEY } = process.env;
const authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'Access denied. No token provided.' });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (err) {
res.status(400).json({ message: 'Invalid token' });
}
};
module.exports = authenticate;
1.5 保护受限制的路由
修改 routes/items.js
文件,添加身份验证中间件:
javascript
const express = require('express');
const { celebrate, Joi } = require('celebrate');
const { MongoClient } = require('mongodb');
const ObjectId = require('mongodb').ObjectId;
const authenticate = require('../middlewares/auth');
const cacheMiddleware = require('../middlewares/cache');
const router = express.Router();
const itemSchema = Joi.object({
name: Joi.string().required(),
description: Joi.string().optional(),
price: Joi.number().min(0).required()
});
const updateItemSchema = Joi.object({
name: Joi.string().optional(),
description: Joi.string().optional(),
price: Joi.number().min(0).optional()
});
router.get('/', authenticate, cacheMiddleware(60), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const query = {};
const cursor = collection.find(query);
if ((await cursor.count()) === 0) {
res.status(200).send('No items found');
} else {
const items = await cursor.toArray();
res.status(200).json(items);
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.post('/', authenticate, celebrate({ body: itemSchema }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const newItem = req.body;
const result = await collection.insertOne(newItem);
res.status(201).json(result.ops[0]);
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.put('/:id', authenticate, celebrate({ body: updateItemSchema, params: Joi.object({ id: Joi.string().required() }) }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const filter = { _id: new ObjectId(req.params.id) };
const update = { $set: req.body };
const result = await collection.updateOne(filter, update);
if (result.matchedCount === 0) {
res.status(404).send('Item not found');
} else {
res.status(200).json({ message: 'Item updated successfully' });
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.delete('/:id', authenticate, celebrate({ params: Joi.object({ id: Joi.string().required() }) }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const filter = { _id: new ObjectId(req.params.id) };
const result = await collection.deleteOne(filter);
if (result.deletedCount === 0) {
res.status(404).send('Item not found');
} else {
res.status(200).json({ message: 'Item deleted successfully' });
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
module.exports = router;
2. 配置 CORS
为了允许跨域请求,我们需要配置 CORS。安装 cors
库:
bash
npm install cors
修改 app.js
文件,添加 CORS 配置:
javascript
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cors = require('cors');
const swaggerUi = require('swagger-ui-express');
const swaggerJSDoc = require('swagger-jsdoc');
const itemsRouter = require('./routes/items');
const authRouter = require('./routes/auth');
const errorHandler = require('./middlewares/error-handler');
const connectDB = require('./config/db');
const logger = require('./middlewares/logger');
const statusMonitor = require('express-status-monitor');
const app = express();
// 配置 Helmet
app.use(helmet());
// 配置 CORS
app.use(cors());
// 日志中间件
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
app.use(express.json()); // 解析 JSON 请求体
// 压缩响应体
app.use(compression());
// 连接 MongoDB
connectDB();
// 性能监控
app.use(statusMonitor());
// 路由
app.use('/items', itemsRouter);
app.use('/auth', authRouter);
// Swagger 配置
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'This is a simple API for managing items.',
},
servers: [
{
url: `http://localhost:${process.env.PORT || 3000}`,
},
],
},
apis: ['./routes/*.js'], // 指定包含 API 注解的文件
};
const specs = swaggerJSDoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
// 错误处理中间件
app.use(errorHandler);
module.exports = app;
3. 添加缓存机制
为了提高性能,我们可以使用 redis
作为缓存层。安装 redis
和 connect-redis
库:
bash
npm install redis connect-redis
3.1 创建 Redis 客户端
在 config
目录下创建 redis.js
文件:
javascript
const redis = require('redis');
const { REDIS_URL } = process.env;
const client = redis.createClient(REDIS_URL);
client.on('error', (err) => {
console.error('Redis Client Error', err);
});
client.connect();
module.exports = client;
3.2 添加缓存中间件
在 middlewares
目录下创建 cache.js
文件:
javascript
const redis = require('../config/redis');
const cacheMiddleware = (duration) => {
return async (req, res, next) => {
const key = req.originalUrl;
const cachedData = await redis.get(key);
if (cachedData) {
res.send(JSON.parse(cachedData));
return;
}
res.sendResponse = res.send;
res.send = (body) => {
redis.setEx(key, duration, JSON.stringify(body));
res.sendResponse(body);
};
next();
};
};
module.exports = cacheMiddleware;
3.3 使用缓存中间件
修改 routes/items.js
文件,添加缓存中间件:
javascript
const express = require('express');
const { celebrate, Joi } = require('celebrate');
const { MongoClient } = require('mongodb');
const ObjectId = require('mongodb').ObjectId;
const authenticate = require('../middlewares/auth');
const cacheMiddleware = require('../middlewares/cache');
const router = express.Router();
const itemSchema = Joi.object({
name: Joi.string().required(),
description: Joi.string().optional(),
price: Joi.number().min(0).required()
});
const updateItemSchema = Joi.object({
name: Joi.string().optional(),
description: Joi.string().optional(),
price: Joi.number().min(0).optional()
});
router.get('/', authenticate, cacheMiddleware(60), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const query = {};
const cursor = collection.find(query);
if ((await cursor.count()) === 0) {
res.status(200).send('No items found');
} else {
const items = await cursor.toArray();
res.status(200).json(items);
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.post('/', authenticate, celebrate({ body: itemSchema }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const newItem = req.body;
const result = await collection.insertOne(newItem);
res.status(201).json(result.ops[0]);
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.put('/:id', authenticate, celebrate({ body: updateItemSchema, params: Joi.object({ id: Joi.string().required() }) }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const filter = { _id: new ObjectId(req.params.id) };
const update = { $set: req.body };
const result = await collection.updateOne(filter, update);
if (result.matchedCount === 0) {
res.status(404).send('Item not found');
} else {
res.status(200).json({ message: 'Item updated successfully' });
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
router.delete('/:id', authenticate, celebrate({ params: Joi.object({ id: Joi.string().required() }) }), async (req, res, next) => {
try {
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
console.log('Connected to MongoDB');
const database = client.db('myFirstDatabase');
const collection = database.collection('items');
const filter = { _id: new ObjectId(req.params.id) };
const result = await collection.deleteOne(filter);
if (result.deletedCount === 0) {
res.status(404).send('Item not found');
} else {
res.status(200).json({ message: 'Item deleted successfully' });
}
} catch (err) {
next(err);
} finally {
await client.close();
}
});
module.exports = router;
4. 性能监控
为了更好地监控应用的性能,我们可以使用 express-status-monitor
库。安装 express-status-monitor
:
javascript
npm install express-status-monitor
修改 app.js
文件,添加性能监控中间件:
javascript
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cors = require('cors');
const swaggerUi = require('swagger-ui-express');
const swaggerJSDoc = require('swagger-jsdoc');
const itemsRouter = require('./routes/items');
const authRouter = require('./routes/auth');
const errorHandler = require('./middlewares/error-handler');
const connectDB = require('./config/db');
const logger = require('./middlewares/logger');
const statusMonitor = require('express-status-monitor');
const app = express();
// 配置 Helmet
app.use(helmet());
// 配置 CORS
app.use(cors());
// 日志中间件
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`);
next();
});
app.use(express.json()); // 解析 JSON 请求体
// 压缩响应体
app.use(compression());
// 连接 MongoDB
connectDB();
// 性能监控
app.use(statusMonitor());
// 路由
app.use('/items', itemsRouter);
app.use('/auth', authRouter);
// Swagger 配置
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'This is a simple API for managing items.',
},
servers: [
{
url: `http://localhost:${process.env.PORT || 3000}`,
},
],
},
apis: ['./routes/*.js'], // 指定包含 API 注解的文件
};
const specs = swaggerJSDoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
// 错误处理中间件
app.use(errorHandler);
module.exports = app;
项目结构
确保项目结构如下:
javascript
my-app/
├── node_modules/
├── public/
│ └── index.html
├── routes/
│ ├── items.js
│ └── auth.js
├── models/
│ ├── item.js
│ └── user.js
├── middlewares/
│ ├── error-handler.js
│ ├── logger.js
│ ├── auth.js
│ └── cache.js
├── config/
│ ├── db.js
│ └── redis.js
├── .env
├── app.js
└── index.js
运行项目
确保 MongoDB 和 Redis 服务已启动。在项目根目录下运行以下命令启动应用:
bash
npm install node index.js
访问 http://localhost:3000/api-docs
查看 Swagger 文档,访问 http://localhost:3000/status
查看性能监控页面。