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

​🌈个人主页:前端青山
🔥系列专栏: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 查看性能监控页面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/909593.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ThingsBoard规则链节点:Push to Edge节点详解

引言 1. Push to Edge 节点简介 2. 节点配置 2.1 基本配置示例 3. 使用场景 3.1 边缘计算 3.2 本地数据处理 3.3 实时响应 4. 实际项目中的应用 4.1 项目背景 4.2 项目需求 4.3 实现步骤 5. 总结 引言 ThingsBoard 是一个开源的物联网平台,提供了设备管…

第二届计算机网络技术与电子信息工程国际学术会议(CNTEIE 2024,12月6-8日)

第二届计算机网络技术与电子信息工程国际学术会议(CNTEIE 2024) 2024 2nd International Conference on Computer Network Technology and Electronic and Information Engineering 官方信息 会议官网:www.cnteie.org 2024 2nd Internation…

A013-基于SpringBoot的宽带业务管理系统的设计与实现

🙊作者简介:在校研究生,拥有计算机专业的研究生开发团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取,记得注明来意哦~🌹 赠送计算机毕业设计600…

「C/C++」C/C++标准库 之 #include<cstddef> 常用定义和宏

✨博客主页何曾参静谧的博客📌文章专栏「C/C」C/C程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…

Vue3-子传父

1. 主组件 App.vue(父组件) 在 App.vue 中,我们先引入了子组件 SonCom,这个小家伙将在父组件中出场。 接着,我们写了一个叫 getMessage 的函数。这个函数的任务很简单——接收子组件传来的消息,然后用 con…

网络--传输层协议--TCP

1、TCP协议的特性 面向连接(需要建立连接,才能继续通信) 面向字节流的一对一通信模式 通过流量控制和拥塞控制 -> 确保可靠传输 报头大小20字节(若带选项,最大60字节) 2、TCP报文字段 16位源端口号 和 16位目的端口号 -- 2 + 2 = 4 字节 32位序号 和 32位确认序…

【Python】计算机视觉应用:OpenCV库图像处理入门

计算机视觉应用:OpenCV库图像处理入门 在当今的数字化时代,计算机视觉(Computer Vision)已经渗透到各行各业,比如自动驾驶、智能监控、医疗影像分析等。而 Python 的 OpenCV 库(Open Source Computer Visi…

【Ai测评】GPT Search偷偷上线,向Google和微软发起挑战!

最近,OpenAI 又推出了一个令人兴奋的新功能——GPT Search,已经正式上线了! 功能介绍 GPT Search:为你带来全新搜索体验 目前,桌面端和移动端应用程序已经全面上线,所有 GPT Plus 和 Team 用户都可以立即…

iOS用rime且导入自制输入方案

iPhone 16 的 cantonese 只能打传统汉字,没有繁简转换,m d sh d。考虑用「仓」输入法 [1] 使用 Rime 打字,且希望导入自制方案 [2]。 仓输入法有几种导入方案的方法,见 [3],此处记录 wifi 上传法。准备工作&#xff1…

【数据结构】算法的时间复杂度和空间复杂度

写在前面 在我们学习数据结构之前,我们肯定就听过某某大神说,我的排序算法的时间复杂度只需要O(logN),空间复杂度只需要O(1)…听的好唬人,但是实际也真牛。那其中时间复杂度和空间复杂度是什么呢?不才这篇笔记就深入讲…

基于MATLAB的农业病虫害识别研究

matlab有处理语音信号的函数wavread,不过已经过时了,现在处理语音信号的函数名称是audioread选取4.wav进行处理(只有4的通道数为1) 利用hamming窗设计滤波器 Ham.m function [N,h,H,w] Ham(fp,fs,fc)wp 2*pi*fp/fc;ws 2*pi*…

MongoDB基础介绍以及从0~1语法介绍

目录 MongoDB 教程导读 NoSQL 简介 关系型数据库遵循ACID规则 分布式系统 分布式计算的优点 分布式计算的缺点 什么是NoSQL? 为什么使用NoSQL ? RDBMS vs NoSQL NoSQL 简史 CAP定理(CAP theorem) NoSQL的优点/缺点 BASE ACID vs BASE N…

Oracle简介、环境搭建和基础DML语句

第一章 ORACLE 简介 1.1 什么是 ORACLE ORACLE数据库系统是美国ORACLE 公司(甲骨文)提供的以分布式数据库为核心的一组软件产品,是目前最流行的客户/服务器体系结构的数据库之一。 英文官网:Database | Oracle 中文官网&#xff…

前端好用的网站分享——CSS(持续更新中)

1.CSS Scan 点击进入CSS Scan CSS盒子阴影大全 2.渐变背景 点击进入color.oulu 3.CSS简化压缩 点击进入toptal 4.CSS可视化 点击进入CSS可视化 这个强推,话不多说,看图! 5.Marko 点击进入Marko 有很多按钮样式 6.getwaves 点击进入getwaves 生…

黑马官网2024最新前端就业课V8.5笔记---HTML篇

Html 定义 HTML 超文本标记语言——HyperText Markup Language。 标签语法 标签成对出现&#xff0c;中间包裹内容<>里面放英文字母&#xff08;标签名&#xff09;结束标签比开始标签多 /拓展 &#xff1a; 双标签&#xff1a;成对出现的标签 单标签&#xff1a;只有开…

6:arm condition code flags详细的讲解

目录 6.1 arm的 condition code flag 的详细讲解 6.1.1C 6.1.2Z 6.1.3N 6.1.4V 6.1 arm的 condition code flag 的详细讲解 在这篇文章中&#xff0c;我更加严格与严谨的讲解一下 arm的四个condition code flags&#xff0c;因为这个在汇编中还是非常重要的。 6.1.1C 在…

其他节点使用kubectl访问集群,kubeconfig配置文件 详解

上述两种方式&#xff1a;可使用kubectl连接k8s集群。 $HOME/.kube/config 是config文件默认路径&#xff0c;要么直接定义环境变量&#xff0c;要么就直接把文件拷过去 config文件里面&#xff0c;定义了context&#xff0c;里面指定了用户和对应的集群信息&#xff1a; ku…

新世联科技:NG2-A-7在DAC空气捕集提取CO2的应用

一、DAC空气捕集提取CO2的介绍 直接空气碳捕获&#xff08;Direct Air Capture&#xff0c;简称DAC&#xff09;是一种直接从大气中提取二氧化碳的技术。 二、DAC空气捕集提取CO2的前景 从大气中提取的这种二氧化碳可以作为循环经济的一部分以各种不同方式使用。未来&#xf…

十四届蓝桥杯STEMA考试Python真题试卷第二套第五题

来源:十四届蓝桥杯STEMA考试Python真题试卷第二套编程第五题 本题属于迷宫类问题,适合用DFS算法解决,解析中给出了Python中 map() 和列表推导式的应用技巧。最后介绍了DFS算法的两种常见实现方式——递归实现、栈实现,应用场景——迷宫类问题、图的连通性、树的遍历、拓朴排…

js WebAPI黑马笔记(万字速通)

此笔记来自于黑马程序员&#xff0c;pink老师yyds 复习&#xff1a; splice() 方法用于添加或删除数组中的元素。 注意&#xff1a; 这种方法会改变原始数组。 删除数组&#xff1a; splice(起始位置&#xff0c; 删除的个数) 比如&#xff1a;1 let arr [red, green, b…