Node.js-增强 API 安全性和性能优化

​🌈个人主页:前端青山

🔥系列专栏: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 安装依赖

安装 jsonwebtokenbcryptjs 库:

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 作为缓存层。安装 redisconnect-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 查看性能监控页面。

相关推荐
2401_846535953 分钟前
Scala项目(图书管理系统)
开发语言·后端·scala
锅包肉的九珍4 分钟前
Scala图书管理系统
开发语言·后端·scala
Odoo老杨9 分钟前
Odoo 免费开源 ERP:通过 JavaScript 创建对话框窗口的技术实践分享
javascript·odoo·数字化转型·erp·企业信息化
SomeB1oody11 分钟前
【Rust自学】5.1. 定义并实例化struct
开发语言·后端·rust
Kika写代码18 分钟前
【微信小程序】2|轮播图 | 我的咖啡店-综合实训
前端·微信小程序·小程序
red润24 分钟前
使用 HTML5 Canvas 实现动态蜈蚣动画
前端·html·html5
sg_knight32 分钟前
VSCode如何修改默认扩展路径和用户文件夹目录到D盘
前端·ide·vscode·编辑器·web
云空33 分钟前
《解锁 Python 数据挖掘的奥秘》
开发语言·python·数据挖掘
一个处女座的程序猿O(∩_∩)O42 分钟前
完成第一个 Vue3.2 项目后,这是我的技术总结
前端·vue.js