跳转至

第29章:中间件

在构建你的应用程序的过程中,你经常会发现有必要将你自己的步骤集成到请求管道中。完成这个任务的最常见的机制是使用一个或多个中间件。它们允许你做这样的事情。

  • 记录传入的请求。
  • 捕捉错误并显示信息。
  • 对特定路线的流量进行速率限制。

中间件实例位于你的路由器和连接到服务器的客户端之间。这允许他们在传入的请求到达你的控制器之前,查看并有可能改变这些请求。一个中间件实例可以选择通过生成自己的响应来提前返回,也可以将请求转发到链中的下一个响应者。最后的响应者总是你的路由器。当下一个响应者的响应产生时,中间件可以做任何它认为必要的修改,或者选择将其原封不动地转发给客户端。这意味着每个中间件实例都可以控制传入的请求传出的响应。

img

正如你在上图中看到的,你的应用程序中的第一个中间件实例--中间件A--首先接收来自客户端的传入请求。然后,第一个中间件可以选择将这个请求传递给下一个中间件--中间件B--以此类推。

最终,一些组件会产生一个响应,然后以相反的方向穿越中间件回来。请注意,这意味着第一个中间件收到的响应是最后的。

Middleware的协议相当简单,应该可以帮助你更好地理解前面的图:

public protocol Middleware {
  func respond(
    to request: Request, 
    chainingTo next: Responder
  ) -> EventLoopFuture<Response>
}

在中间件A的情况下,request是来自客户端的传入数据,而next是中间件B。中间件A返回的异步响应直接到客户端。

对于中间件Brequest是从中间件A传递过来的请求,next是路由器。由中间件B返回的未来响应会转给中间件A

Vapor的中间件

Vapor包括一些开箱即用的中间件。本节向你介绍可用的选项,让你了解中间件的常用用途。

错误中间件

Vapor中最常用的中间件是ErrorMiddleware。它负责将同步和异步的Swift错误转换为HTTP响应。未捕获的错误会导致HTTP服务器立即关闭连接,并打印一个内部错误日志。

使用ErrorMiddleware可以确保你抛出的所有错误都被呈现为适当的HTTP响应。

在生产模式下,ErrorMiddleware将所有的错误转换为不透明的500 Internal Server Error响应。这对保持你的应用程序的安全性很重要,因为错误可能包含敏感信息。

你可以选择提供不同的错误响应,将你的错误类型与AbortError一致,允许你指定HTTP状态代码和错误信息。你也可以使用Abort,一个符合AbortError的具体错误类型。例如:

throw Abort(.badRequest, "Something's not quite right.")

文件中间件

另一种常见的中间件是FileMiddleware。这种中间件从你的应用程序目录中的Public文件夹提供文件。当你使用Vapor创建前端网站时,这很有用,因为它可能需要图片或样式表等静态文件。

其他中间件

Vapor还提供了一个SessionsMiddleware,负责跟踪与连接客户的会话。其他软件包可能提供中间件来帮助它们集成到你的应用程序中。例如,Vapor的认证包包含中间件,用于保护你的路由,使用基本密码、简单的不记名令牌,甚至JWTJSON Web Token)。

示例:Todo API

现在你已经了解了各种类型的中间件的功能,你已经准备好学习如何配置它们以及如何创建自己的自定义中间件类型。

为了做到这一点,你将实现一个基本的Todo列表API。这个API有三个路由:

$ swift run Run routes
+--------+--------------+
| GET    | /todos       |
+--------+--------------+
| POST   | /todos       |
+--------+--------------+
| DELETE | /todos/:todo |
+--------+--------------+

你将为这个项目创建和配置两种不同的中间件类型:

  1. LogMiddleware:记录传入请求的响应时间。
  2. SecretMiddleware:通过要求一个秘密密钥来保护私人路线不被擅自访问。

日志中间件

你要创建的第一个中间件将记录传入的请求。它将为每个请求显示以下信息。

  • 请求方法
  • 请求路径
  • 响应状态
  • 产生响应所需的时间

在终端中打开启动项目目录,并通过输入为其生成一个Xcode项目:

open Package.swift

一旦Xcode打开,导航到Middleware/LogMiddleware.swift。在那里你会发现一个空的LogMiddleware类。

暂时忽略TimeInterval扩展;你将在后面使用它。

首先,使LogMiddleware符合Middleware协议。只有一个方法是必需的:respond(to:chainingTo:)

现在,中间件将只是记录传入请求的描述。用以下方法替换LogMiddleware

final class LogMiddleware: Middleware {
  // 1
  func respond(
    to req: Request, 
    chainingTo next: Responder
  ) -> EventLoopFuture<Response> {
    // 2
    req.logger.info("\(req)")
    // 3
    return next.respond(to: req)
  }
}

下面是你刚刚添加的代码的分类:

  1. 实现Middleware协议要求。
  2. 将请求的描述作为信息日志发送到记录器。
  3. 将传入的请求转发给下一个响应者。

现在你已经创建了一个自定义的中间件,你需要把它添加到你的应用程序中。打开configure.swift,在configure(_:)的开头添加以下一行:

app.middleware.use(LogMiddleware())

这将在全球范围内启用LogMiddleware。这里的排序很重要,因为Middleware是按照它们被添加的顺序运行的。

最后,建立并运行你的应用程序,然后用curlGET /todos发出请求:

curl localhost:8080/todos

看看你运行中的应用程序的日志输出。你会看到类似于下面的内容:

[ INFO ] GET /todos HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
 [request-id: 4D528CE6-8C10-443A-A5BB-9A6F2BB3A6E7]

这是一个很好的开始! 但你可以改进LogMiddleware,以提供更有用、可读的输出。打开LogMiddleware.swift,用以下方法替换respond(to:chainingTo:)的实现:

func respond(
  to req: Request, 
  chainingTo next: Responder
) -> EventLoopFuture<Response> {
  // 1
  let start = Date()
  return next.respond(to: req).map { res in
    // 2
    self.log(res, start: start, for: req)
    return res
  }
}

// 3
func log(_ res: Response, start: Date, for req: Request) {
  let reqInfo = "\(req.method.string) \(req.url.path)"
  let resInfo = "\(res.status.code) " + 
    "\(res.status.reasonPhrase)"
  // 4
  let time = Date()
    .timeIntervalSince(start)
    .readableMilliseconds
  // 5
  req.logger.info("\(reqInfo) -> \(resInfo) [\(time)]")
}

以下是新方法的工作细节:

  1. 首先,创建一个开始时间。在做任何其他工作之前做这个,以获得最准确的响应时间测量。
  2. 不要直接返回响应,而是映射未来的结果,这样你就可以访问Response对象。将其传递给log(_:start:for:)
  3. 这个方法使用响应开始日期来记录一个传入请求的响应。
  4. 使用timeIntervalSince(_:)和文件底部TimeInterval上的扩展名,生成一个可读的时间。
  5. 记录信息字符串。

现在你已经更新了LogMiddleware,建立并运行和curl GET /todos,再次。

curl localhost:8080/todos

如果你检查你的应用程序的输出,你会看到一个新的、更简洁的输出格式。

[ INFO ] GET /todos -> 200 OK [1.7ms] [request-id: ...]

秘密中间件

现在你已经学会了如何创建中间件并全局应用,你将学习如何将中间件应用于特定的路由。

Todo List API中的两个路由可以对数据库进行修改:

  • POST /todos
  • DELETE /todos/:id

如果这是一个公开的API,你会希望用中间件来保护这些路由的秘密密钥。这正是SecretMiddleware所要做的。

打开Middleware/SecretMiddleware.swift,用以下代码替换SecretMiddleware的类定义:

final class SecretMiddleware: Middleware {
  // 1
  let secret: String

  init(secret: String) {
    self.secret = secret
  }

  // 2
  func respond(
    to request: Request,
    chainingTo next: Responder
  ) -> EventLoopFuture<Response> {
    // 3
    guard
      request.headers.first(name: .xSecret) == secret
    else {
      // 4
      return request.eventLoop.makeFailedFuture(
        Abort(
          .unauthorized, 
          reason: "Incorrect X-Secret header."))
    }
    // 5
    return next.respond(to: request)
  }
}

下面是SecretMiddleware的工作原理的详细介绍:

  1. 创建一个存储属性来保存秘钥。
  2. 实现Middleware协议要求。
  3. 根据配置的密匙检查传入请求中的X-Secret头。
  4. 如果标头值不匹配,则抛出一个unauthorizedHTTP状态错误。
  5. 如果头文件匹配,正常情况下,链到下一个中间件。

现在你只需要添加一个方法来创建这个中间件,这样它就可以在你的应用程序中作为一个服务使用。

SecretMiddleware的实现后添加以下代码:

extension SecretMiddleware {
  // 1
  static func detect() throws -> Self {
    // 2
    guard let secret = Environment.get("SECRET") else {
      // 3
      throw Abort(
        .internalServerError, 
        reason: """
          No SECRET set on environment. \
          Use export SECRET=<secret>
          """)
    }
    // 4
    return .init(secret: secret)
  }
}

下面是这个代码的工作原理的细目:

  1. SecretMiddleware添加一个静态的、抛出的方法。
  2. 从环境中获取SECRET的值,如果它存在的话。
  3. 如果环境变量不存在,则抛出一个有用的错误。
  4. 使用配置的秘密初始化一个SecretMiddleware的实例。

是时候使用新的中间件了。打开routes.swift,用以下代码替换POSTDELETE路由:

// 1
try app.group(SecretMiddleware.detect()) { secretGroup in
  // 2
  secretGroup.post("todos", use: todoController.create)
  secretGroup.delete(
    "todos", 
    ":id", 
    use: todoController.delete)
}

下面是这个的作用:

  1. 创建一个由SecretMiddleware包裹的新路由组。
  2. 在新创建的路由组中注册POSTDELETE路由,而不是全局路由器。

在使用你的新中间件之前,你需要设置秘密。在你的项目目录下创建一个名为.env的新文件,并插入以下内容:

SECRET=foo

这将创建SecretMiddleware所需的秘密。Vapor在应用启动时读取.env文件,这是向你的应用注入环境变量的一种方法。最后,你必须设置自定义工作目录,以便Vapor知道在哪里找到.env文件。正如你在前面的章节中所做的那样,编辑TodoAPI的方案。在Run动作下,在Options中,选中Use custom working directory。将路径设置为你的项目目录:

img

建立并运行该应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

  • URL: http://localhost:8080/todos
  • method: POST

添加一个带有名称和值的参数:

  • title: This is a test TODO!

点击Send Request并注意到响应:

{
    "error": true,
    "reason": "Incorrect X-Secret header."
}

中间件正在保护这些路线! 如果你尝试查询GET /todos,你会发现它仍然有效。

X-Secret: foo添加到RESTed的头信息部分,并再次发送请求。现在你会注意到,响应已经改变了。中间件允许这个请求通过控制器,现在它有适当的头信息。

接下来去哪?

中间件对于创建大型网络应用程序是非常有用的。它允许你使用离散的、可重用的组件在全局或仅在少数路由中应用限制和转换。在本章中,你学到了如何创建一个全局的LogMiddleware,显示所有进入你的应用程序的请求的信息。然后你创建了SecretMiddleware,它可以保护选定的路由不被公开访问。

关于使用中间件的更多信息,请务必查看VaporAPI文档