🌈个人主页:前端青山
🔥系列专栏:node.js篇
🔖人终将被年少不可得之物困其一生
依旧青山,本期给大家带来node.js篇专栏内容:node.js-增强 API 安全性和性能优化
前言
在前几篇文章中,我们已经构建了一个基本的 Express API 服务,实现了 CRUD 操作、环境变量管理、日志记录、错误处理和数据库连接优化。本文将继续在这个基础上,进一步增强 API 的安全性和性能优化。我们将添加身份验证、CORS 配置、缓存机制和更详细的性能监控。
目录
前言
1. 添加身份验证
1.1 安装依赖
1.2 创建用户模型
1.3 创建用户注册和登录路由
1.4 添加身份验证中间件
1.5 保护受限制的路由
2. 配置 CORS
3. 添加缓存机制
3.1 创建 Redis 客户端
3.2 添加缓存中间件
3.3 使用缓存中间件
4. 性能监控
项目结构
运行项目
1. 添加身份验证
为了保护 API 的安全性,我们将添加 JWT(JSON Web Token)身份验证。JWT 是一种广泛使用的身份验证机制,可以确保只有经过身份验证的用户才能访问受保护的资源。
1.1 安装依赖
安装 jsonwebtoken
和 bcryptjs
库:
npm install jsonwebtoken bcryptjs
1.2 创建用户模型
在 models
目录下创建 user.js
文件:
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
文件:
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
文件:
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
文件,添加身份验证中间件:
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
库:
npm install cors
修改 app.js
文件,添加 CORS 配置:
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
库:
npm install redis connect-redis
3.1 创建 Redis 客户端
在 config
目录下创建 redis.js
文件:
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
文件:
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
文件,添加缓存中间件:
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
:
npm install express-status-monitor
修改 app.js
文件,添加性能监控中间件:
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;
项目结构
确保项目结构如下:
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 服务已启动。在项目根目录下运行以下命令启动应用:
npm install node index.js
访问 http://localhost:3000/api-docs
查看 Swagger 文档,访问 http://localhost:3000/status
查看性能监控页面。