Easton Notes
知识八股Express

中间件链路:先后顺序、安全、认证、校验

Express 的中间件有哪些类型?

回答重点

在 Express.js 中,中间件的类型主要有以下几种:

1)应用级中间件:直接绑定到 Express 应用实例的中间件。 2)路由级中间件:绑定到 Express 路由实例的中间件。 3)错误处理中间件:专门用于处理错误的中间件,拥有特殊的四个参数签名 (err, req, res, next)。 4)内置中间件:Express 提供的内置中间件,如 express.staticexpress.json。 5)第三方中间件:由社区开发的中间件,可以通过 npm 安装使用。

扩展知识

下面来详细看看每种中间件的特点和使用方法。

1)应用级中间件:这种中间件直接挂载在 express 应用实例上,适用于全局的请求处理。

const express = require('express');
const app = express();
app.use((req, res, next) => {
  console.log('Time:', Date.now());
next();
}
);
app.get('/', (req, res) => {
  res.send('Hello World!');
}
);

2)路由级中间件:这种中间件绑定到 express.Router 实例,可以将多个路由组合在一起。

const express = require('express');
const router = express.Router();
router.use((req, res, next) => {
  console.log('Request URL:', req.originalUrl);
  next();
}
);
router.get('/', (req, res) => {
  res.send('Hello Router!');
}
);
const app = express();
app.use('/router', router);

3)错误处理中间件:这种中间件专门用于处理请求过程中发生的错误。注意,它有四个参数。

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
}
);

4)内置中间件:Express 自身提供了一些内置的中间件,比如 express.static 用于提供静态文件服务,express.json 解析 JSON 格式的请求体。

app.use(express.static('public'));
app.use(express.json());
app.post('/data', (req, res) => {
  res.send(req.body);
}
);

5)第三方中间件:这些是由社区开发并通过 npm 提供的中间件。例如 body-parser 比 Express 内置的 JSON 解析更为强大。

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
  extended: false
}
));
app.use(bodyParser.json());
app.post('/data', (req, res) => {
  res.send(req.body);
}
);

在 Express 中如何实现应用级别、路由级别和错误处理中间件?

回答重点

在 Express 中,中间件的使用非常关键,它决定了请求和响应的处理流程。中间件主要分为三类:应用级别、路由级别和错误处理中间件。

1)应用级别中间件:应用程序实例上使用的中间件,对所有的路由和数据生效。

const express = require('express');
const app = express();
app.use((req, res, next) => {
  console.log('执行应用级别的中间件');
  next();
}
);
// 示范一个简单的路由 app.get('/', (req, res) => {
res.send('Hello World!');
}
);
app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
}
);

2)路由级别中间件:绑定在特定路由上的中间件,通常与 express.Router() 一起使用。

const express = require('express');
const app = express();
const router = express.Router();
router.use((req, res, next) => {
  console.log('执行路由级别的中间件');
  next();
}
);
router.get('/', (req, res) => {
  res.send('Hello from route!');
}
);
app.use('/route', router);
app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
}
);

3)错误处理中间件:处理应用程序中的错误,必须有四个参数(err, req, res, next),重要的是顺序和参数数量。

const express = require('express');
const app = express();
app.use((req, res, next) => {
  // 模拟一个错误
  const err = new Error('Something went wrong!');
  err.status = 500;
  next(err);
}
);
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500);
  res.send('发生了错误:' + err.message);
}
);
app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
}
);

扩展知识

下面扩展一些细节和实际应用场景。

1)多个中间件的使用

  • 中间件可以链式调用,每个中间件通过 next() 函数传递控制权。
const express = require('express');
const app = express();
const middleware1 = (req, res, next) => {
  console.log('Middleware 1');
  next();
}
;
const middleware2 = (req, res, next) => {
  console.log('Middleware 2');
  next();
}
;
app.use(middleware1);
app.use(middleware2);
app.get('/', (req, res) => {
  res.send('Hello World!');
}
);
app.listen(3000);

2)第三方中间件

  • Express 生态系统中有很多有用的第三方中间件,如 body-parsercookie-parser 等。
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
// 解析 JSON 格式的请求体 app.post('/data', (req, res) => {
res.send(`你发送的数据是:${JSON.stringify(req.body)}`);
}
);
app.listen(3000);

3)性能优化与安全

  • 使用中间件时要注意性能优化,尽量减少不必要的中间件调用,提升响应速度。
  • 安全方面,可以使用中间件来保护路由,如使用 helmet 增强安全性。
const helmet = require('helmet');
app.use(helmet());
// 使用 Helmet 来增加 HTTP 头保护应用程序

Express 中间件的顺序对应用有什么影响,如何管理?

回答重点

在 Express 中,中间件(middleware)的顺序是非常重要的,因为请求从上到下按顺序逐个通过中间件。如果中间件的顺序不合理,可能导致一些功能无法正常工作,甚至影响整个应用的行为。例如,如果一个中间件中终止了请求-响应循环(通过 res.send()res.end()),后面的中间件就再也无法被执行了。因此,合理地安排中间件的顺序对于应用的正常运行至关重要。

扩展知识

下面扩展一下为什么中间件顺序如此重要以及如何有效地管理它们。

1)中间件的工作机制

  • 中间件是处理 Express 应用中 HTTP 请求的函数。每个中间件都可以访问请求对象(req)、响应对象(res)以及应用的下一个中间件函数(next)。
  • 中间件函数可以执行三种主要任务:
    1. 结束请求(例如:发送响应)。
    2. 调用 next() 传递控制权给下一个中间件。
    3. 修改请求和响应对象。

2)常见中间件类型

  • 应用级别中间件:通过 app.use() 或 app.METHOD() 绑定。
  • 路由级别中间件:绑定到 express.Router 的实例上。
  • 错误处理中间件:定义四个参数(err, req, res, next)以捕获应用中的错误。
  • 内置中间件:例如 Express 4.x 内置的 express.static
  • 第三方中间件:例如 body-parsermorgan等。

3)中间件顺序对应用的影响

  • 日志记录和统计:通常将这类中间件放在最前面,如 morgan,以确保所有请求都被记录。
  • 安全相关中间件:例如 CORS 配置、Helmet 等通常也在前面,以确保安全策略应用在所有请求上。
  • 解析请求体:例如 body-parser,需要尽早放置,以便后续中间件可以直接访问解析后的数据。
  • 路由定义:路由中间件的定义顺序直接影响请求处理的顺序。
  • 静态资源中间件:如 express.static,通常放在路由定义之后,如果前面已经匹配到一个路由,中间件将不会被执行。

4)管理中间件顺序的最佳实践

  • 分模块管理:将中间件和路由模块化,根据功能分块管理,确保清晰有序。
  • 按功能分组和加载:如保安、解析、日志等中间件按功能分组,确保顺序合理。
  • 使用评论和文档:在代码中充分使用注释,解释中间件的作用和顺序,便于维护。

举个例子,假设我们有以下中间件配置:

const express = require('express');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const app = express();
// 1. 日志记录中间件,放在最前面 app.use(morgan('dev'));
// 2. 解析 JSON 请求体 app.use(bodyParser.json());
// 3. 路由中间件 app.get('/', (req, res) => {
res.send('Hello, world!');
}
);
// 4. 错误处理中间件 app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
}
);
app.listen(3000, () => {
  console.log('Server is running on port 3000');
}
);

在这个例子中,我把日志记录中间件放在最前面,然后是解析请求体的中间件,接着定义了路由,最后添加错误处理中间件。这样保证所有请求都可以按照正确的顺序被处理和记录。

如何在 Express 中使用 Morgan 记录日志

回答重点

在 Express 中使用 Morgan 来记录日志非常简单。Morgan 是一个 HTTP 请求日志记录中间件,可以帮助你记录所有进入你应用的 HTTP 请求。要使用 Morgan,你只需安装这个模块并将其用作 Express 中间件即可。

以下是简单的步骤:

1)首先,你需要安装 Morgan 模块,可以使用 npm(Node Package Manager)来安装:

npm install morgan

2)安装完成后,可以在你的 Express 应用中引入并使用 Morgan:

const express = require('express');
const morgan = require('morgan');
const app = express();
// 使用预定义的日志记录格式,也可以自定义 app.use(morgan('dev'));
app.get('/', (req, res) => {
  res.send('Hello World!');
}
);
app.listen(3000, () => {
  console.log('Server is running on port 3000');
}
);

在上面的代码中,我使用了 'dev' 预定义格式进行日志记录,这种格式适用于开发环境,它会在控制台输出较为详细的日志信息。

扩展知识

1)预定义和自定义格式: Morgan 提供了多种预定义的日志格式,比如 'combined'、'common'、'dev'、'short' 和 'tiny'。你还可以通过自定义格式来满足更具体的需求:

app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

2)将日志输出到文件: 如果你希望将日志记录到文件而不仅仅是控制台,可以结合 fs 模块来创建写入流:

const fs = require('fs');
const path = require('path');
// 创建日志文件写入流
const logStream = fs.createWriteStream(path.join(__dirname, 'access.log'), {
  flags: 'a'
}
);
app.use(morgan('combined', {
  stream: logStream
}
));

3)结合其他中间件: 一般来说,你可能还会结合其他中间件来处理各种请求,比如 body-parsercookie-parser 等。Morgan 大多数情况下会放在这些中间件之前,以便记录所有的请求信息。

const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(cookieParser());

4)日志级别条件过滤: 你可以根据不同的条件来控制日志记录,比如只在开发环境记录日志:

if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

5)与日志管理工具结合: 对于更复杂和专业的日志管理,可以使用更加完善的日志工具,比如 Winston,然后将 Morgan 的日志输出到 Winston

const winston = require('winston');
const {
  createLogger, transports, format
} = winston;
const logger = createLogger({
  level: 'info', format: format.combine( format.colorize(), format.simple() ), transports: [new transports.Console()]
}
);
app.use(morgan('combined', {
  stream: {
    write: (message) => logger.info(message.trim())
  }
}
));

在这里,我用了 winston 来处理 Morgan 记录的日志信息,并将其输出到控制台。

在 Express 中如何使用 Helmet 提高安全性

回答重点

在 Express 中使用 Helmet 提高安全性的方法非常简单。Helmet 是一个帮助你保护 Express 应用程序的中间件,它通过设置适当的 HTTP 头来实现这一点。以下是我实现的步骤:

1)首先,确保你安装了 Helmet 库。可以使用 npm 或 yarn 来安装它:

npm install helmet

或者

yarn add helmet

2)接下来,在你的 Express 应用中引入并使用 Helmet:

const express = require('express');
const helmet = require('helmet');
const app = express();
// 使用 Helmet app.use(helmet());
// 定义路由和中间件 app.get('/', (req, res) => {
res.send('Hello world!');
}
);
// 启动服务器 app.listen(3000, () => {
console.log('Server is running on port 3000');
}
);

通过这两步,你已经成功地在 Express 应用中集成了 Helmet,它会自动为你设置各种安全相关的 HTTP 头,帮助保护你的应用免受常见的攻击。

扩展知识

Helmet 能做的远不止这些,下面进一步扩展一些关于 Helmet 的知识和高级用法:

1)Helmet 的具体功能模块:Helmet 包含多个子模块,每个子模块负责具体的安全措施。例如:

  • helmet.contentSecurityPolicy(): 设置内容安全策略(CSP),防止跨站脚本(XSS)和其他代码注入攻击。
  • helmet.xssFilter(): 启用 XSS 过滤器。
  • helmet.hidePoweredBy(): 移除 X-Powered-By 头,防止攻击者获取服务器信息。

2)自定义配置:你也可以对子模块进行细粒度的配置,比如禁用某些头信息或者自定义某些策略:

const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"], scriptSrc: ["'self'", "'trusted-scripts.com'"]
    }
  }
  , hidePoweredBy: {
    setTo: 'PHP 4.2.0'
  }
  , frameguard: {
    action: 'deny'
  }
}
));

3)其他安全中间件:虽然 Helmet 提供了很好的基础安全措施,但对于一些高级主题,比如 JWT 身份验证、CSRF 保护等,你可能需要引入其他中间件。例如:

  • csurf 中间件用于防止跨站请求伪造(CSRF)攻击。
  • express-rate-limit 用于限制重试次数,防止暴力攻击。

4)在生产环境中的最佳实践:对于生产环境,我会建议大家更细致地配置 Helmet 和其他安全相关中间件,同时定期更新依赖、审计代码和开展安全测试。

如何在 Express 中实现 CSRF 防护?

回答重点

在 Express 中实现 CSRF(Cross-Site Request Forgery)防护,主要可以使用 csurf 这个中间件。以下是实现 CSRF 防护的具体步骤:

1)安装 csurf

npm install csurf

2)在你的 Express 应用中配置 csurf

const express = require('express');
const cookieParser = require('cookie-parser');
const csurf = require('csurf');
const app = express();
// 中间件:解析 Cookie app.use(cookieParser());
// 中间件:CSRF防护 app.use(csurf({
cookie: true
}
));
// 路由处理 app.get('/form', (req, res) => {
res.send(` <form action="/submit" method="POST"> <input type="hidden" name="_csrf" value="${req.csrfToken()}"> <button type="submit">Submit</button> </form> `);
}
);
app.post('/submit', (req, res) => {
  res.send('Form submission successful!');
}
);
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    res.status(403).send('Form tampered with');
  } else {
  next(err);
}
}
);
app.listen(3000, () => {
  console.log('Server running on port 3000');
}
);

扩展知识

除了在代码中使用 csurf 中间件外,关于 CSRF 防护还有一些值得注意的扩展知识点:

1)CSRF 防护原理:CSRF 防护通常使用 CSRF Token 的方式来防护。每次生成和验证表单提交中附带的 CSRF Token,可以保证请求是从受信任的页面发出的,而不是第三方伪造的请求。

2)把握 Token 的传递方式:在实际的应用中,CSRF Token 可以通过不同的方式传递到客户端,比如通过 cookie、hidden 表单字段或者 HTTP header。采用哪种方式取决于具体的应用场景和安全需求。

3)注意跨域的安全问题:在使用 CSRF Token 的过程中,务必确保 Token 的传输是安全的,避免被中间人攻击、劫持或者泄露。

4)防护策略的补充:虽然 CSRF Token 是最常用的防护措施,但可以结合其他方法来增强防护效果,比如通过检查 Referer 或 Origin header,限制从其他站点发出的请求。

5)其他防护工具:除了 csurf,市面上还有其他一些工具和库支持 CSRF 防护,比如 OWASP 提供的一些安全指南和工具库,可以结合使用以增强应用的安全性。

Express 中如何实现请求参数的验证和校验?

在 Express 中实现请求参数的验证和校验,可以使用 express-validator 这个中间件库。这个库提供了丰富的验证和校验功能,可以很方便地集成到 Express 应用中。

以下是一个简单的实现步骤:

1)首先,安装 express-validator

npm install express-validator

2)在你的 Express 应用中,引入相关模块:

const express = require('express');
const {
  body, validationResult
} = require('express-validator');
const app = express();
app.use(express.json());

3)定义一个路由,并在其中添加参数验证和校验:

app.post('/user', [ body('username').isLength({
    min: 5
  }
  ).withMessage('Username must be at least 5 chars long'), body('email').isEmail().withMessage('Email is not valid'), body('age').isInt({
    min: 1
  }
  ).withMessage('Age must be a positive integer'), ], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array()
    }
    );
  } res.send('User is valid');
}
);

4)启动 Express 应用:

app.listen(3000, () => {
  console.log('Server is running on port 3000');
}
);

扩展知识

Express 中的请求参数验证和校验是非常重要的,它确保了服务器接收到的数据是符合预期的,防止了许多潜在的错误和安全问题。下面扩展一些相关的知识点:

1)自定义校验: 除了自带的验证方法,express-validator 还支持自定义校验规则,你可以根据特定的业务需求来编写。

app.post('/product', [ body('price').custom(value => {
    if (value < 0) {
      throw new Error('Price must be a positive number');
    }
    return true;
  }
  ), ], (req, res) => {
  // 跟上例相同的错误处理逻辑
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array()
    }
    );
  } res.send('Product is valid');
}
);

2)参数级别校验: 除了在 body 中的参数,你还可以对查询参数 query,路径参数 params 进行校验。

app.get('/user/:id', [ param('id').isUUID().withMessage('Invalid User ID'), query('age').optional().isInt({
    min: 1
  }
  ).withMessage('Age must be a positive integer if provided'), ], (req, res) => {
  // 错误处理逻辑
}
);

3)性能优化: 在实际项目中,参数验证可能会变得比较繁琐,影响性能及可读性。因此,你可以将校验规则抽离成单独的文件,这样有助于代码的组织和维护。

// validations/userValidations.js
const {
  body
} = require('express-validator');
const userValidations = [ body('username').isLength({
    min: 5
  }
  ).withMessage('Username must be at least 5 chars long'),
  // 其他验证规则 ];
module.exports = userValidations;
// 在路由中使用
const userValidations = require('./validations/userValidations');
app.post('/user', userValidations, (req, res) => {
  // 错误处理逻辑
}
);

在 Express 中如何实现用户认证和授权?

回答重点

在 Express 中实现用户认证和授权主要分为两个步骤:认证(Authentication)和授权(Authorization)。通常,我们会使用 jsonwebtoken 来处理 JWT(JSON Web Tokens)以实现这两个步骤。

1)用户认证:登录时,验证用户凭证(例如用户名和密码),生成 JWT 并返回给客户端。 2)用户授权:客户端每次请求时,使用中间件验证 JWT,以确定用户是否有访问权限。

我来给你举个简单的例子,确保每一步都清晰明确。

用户认证

1)用户发送用户名和密码进行登录。 2)服务器验证用户名和密码。 3)如果验证成功,服务器生成一个 JWT 并返回给客户端。

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();
const SECRET_KEY = 'your_secret_key';
app.use(bodyParser.json());
// 模拟的用户数据
const users = [{
    id: 1, username: 'test', password: 'password'
  }];
app.post('/login', (req, res) => {
  const {
    username, password
  } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (user) {
    const token = jwt.sign({
      id: user.id
    }
    , SECRET_KEY, {
      expiresIn: '1h'
    }
    );
    res.json({
      token
    }
    );
  } else {
  res.status(401).json({
    message: 'Invalid credentials'
  }
  );
}
}
);
app.listen(3000, () => console.log('Server running on port 3000'));

用户授权

1)客户端将 JWT 附加在请求头上发送到服务器。 2)服务器通过中间件解析和验证 JWT。 3)如果 JWT 合法且未过期,允许访问资源;否则,拒绝访问。

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your_secret_key';
const authenticateJWT = (req, res, next) => {
  const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
if (token) {
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.sendStatus(403);
    } req.user = user;
    next();
  }
  );
} else {
res.sendStatus(401);
}
}
;
app.get('/protected', authenticateJWT, (req, res) => {
  res.json({
    message: 'This is a protected route'
  }
  );
}
);
app.listen(3000, () => console.log('Server running on port 3000'));

扩展知识

1)密码加密: 在实际项目中,密码不应该以明文存储。常用的加密方法如 bcrypt。

const bcrypt = require('bcrypt');
const hashedPassword = bcrypt.hashSync('password', 10);
// 加密
const isMatch = bcrypt.compareSync('input_password', hashedPassword);
// 验证

2)刷新 Token(Refresh Token): 为了处理 JWT 过期的问题,一般会使用刷新 Token。

3)权限划分: 根据不同用户的角色(如 admin,user)来细化权限。

4)注入攻击防范: 防止 JWT 被篡改,选择适当的加密算法(如 HS256, RS256)。

5)日志和审计: 记录所有的登录和重要操作,方便后期审计。

在 Express 中如何使用 Passport.js 进行身份认证

回答重点

在 Express 中使用 Passport.js 进行身份认证主要分为以下几个步骤:

1)安装所需的包: 你需要安装 passport 和相应的认证策略包(比如 passport-local)。

npm install passport passport-local

2)配置 Passport: 你需要设置 Passport 的策略、中间件、以及用户序列化与反序列化。以下是简单的配置示例:

const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();
// Initialize Passport app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy( function(username, password, done) {
  User.findOne({
    username: username
  }
  ,
  function (err, user) {
    if (err) {
      return done(err);
    } if (!user) {
    return done(null, false, {
      message: 'Incorrect username.'
    }
    );
  } if (!user.verifyPassword(password)) {
  return done(null, false, {
    message: 'Incorrect password.'
  }
  );
}
return done(null, user);
}
);
}
));
// Serialize and deserialize user passport.serializeUser(function(user, done) {
done(null, user.id);
}
);
passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  }
  );
}
);

3)设置路由: 将认证策略应用到路由中,这样你可以用 Passport.js 进行身份认证。

app.post('/login', passport.authenticate('local', {
  successRedirect: '/', failureRedirect: '/login', failureFlash: true
}
) );

扩展知识

1)不同认证策略 Passport.js 支持很多类型的认证策略,例如 OAuth、JWT、OpenID 等等。每种策略都有相对应的库和配置方法。例如:

  • 使用 JWT 策略:
npm install passport-jwt
然后相应地配置 JWT 策略,比如:
const JWTStrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
passport.use(new JWTStrategy({
  jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), secretOrKey : 'your_jwt_secret'
}
,
function (jwtPayload, cb) {
  return User.findById(jwtPayload.id) .then(user => {
    return cb(null, user);
  }
  ) .catch(err => {
    return cb(err);
  }
  );
}
));

2)处理会话数据 在上述配置中,passport.serializeUserpassport.deserializeUser 方法是处理用户会话数据的关键。你可以自定义这些方法来满足复杂的需求,如存储更多的用户信息或支持分布式会话存储。

3)错误处理 使用 Passport.js 进行认证时,错误处理也很重要。上述示例中的 failureFlash: true 就是表示在认证失败时显示错误消息。你也可以自定义错误处理逻辑:

app.post('/login', function(req, res, next) {
  passport.authenticate('local', function(err, user, info) {
    if (err) {
      return next(err);
    } if (!user) {
    return res.status(401).send(info.message);
  } req.logIn(user, function(err) {
  if (err) {
    return next(err);
  }
  return res.redirect('/');
}
);
}
)(req, res, next);
}
);

4)保护路由 你可能需要保护某些路由,只有经过认证的用户才能访问这些路由。你可以使用 Passport 的 isAuthenticated 方法来实现:

function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  } res.redirect('/login');
} app.get('/profile', ensureAuthenticated, function(req, res){
res.render('profile', {
  user: req.user
}
);
}
);

On this page