跳转至

第21章:验证

在前面的章节中,你建立了一个功能齐全的API和网站。用户可以发送请求,并填写表格来创建缩写、类别和其他用户。在本章中,你将学习如何使用Vapor的验证库来验证用户发送给应用程序的一些信息。你将在网站上创建一个注册页面供用户注册。然后你将验证这个表单中的数据,如果数据不正确,将显示一个错误信息。

注册页面

Resources/Views中创建一个新文件,名为register.leaf。这就是注册页面的模板。打开register.leaf,将其内容替换为以下内容:

#extend("base"):
  #export("content"):
    <h1>#(title)</h1>

    <form method="post">
        <div class="form-group">
        <label for="name">Name</label>
        <input type="text" name="name" class="form-control"
        id="name"/>
        </div>

        <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" class="form-control"
        id="username"/>
        </div>

        <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password"
        class="form-control" id="password"/>
        </div>

        <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input type="password" name="confirmPassword"
        class="form-control" id="confirmPassword"/>
        </div>

        <button type="submit" class="btn btn-primary">
        Register
        </button>
    </form>
  #endexport
#endextend

这与创建首字母缩写和登录的模板非常相似。该模板包含四个输入字段:

  • name
  • username
  • password
  • password confirmation

保存该文件。接下来,在Xcode中,打开WebsiteController.swift,在文件的底部,为注册页面添加以下内容:

struct RegisterContext: Encodable {
  let title = "Register"
}

接下来,在logoutHandler(_:)下面,为注册页面添加以下路由处理器:

func registerHandler(_ req: Request) -> EventLoopFuture<View> {
  let context = RegisterContext()
  return req.view.render("register", context)
}

像其他路由处理程序一样,这将创建一个上下文,然后调用render(_:_:)来渲染register.leaf

接下来,为注册的POST请求创建Content,在WebsiteController.swift的末尾添加如下内容:

struct RegisterData: Content {
  let name: String
  let username: String
  let password: String
  let confirmPassword: String
}

这个Content类型与从注册POST请求中收到的预期数据一致。变量与register.leaf中的输入名称一致。接下来,为这个POST请求创建一个路由处理程序,在registerHandler(_:)后面添加以下内容:

// 1
func registerPostHandler(
  _ req: Request
) throws -> EventLoopFuture<Response> {
  // 2
  let data = try req.content.decode(RegisterData.self)
  // 3
  let password = try Bcrypt.hash(data.password)
  // 4
  let user = User(
    name: data.name,
    username: data.username,
    password: password)
  // 5
  return user.save(on: req.db).map {
    // 6 
    req.auth.login(user)
    // 7
    return req.redirect(to: "/")
  }
}

下面是路线处理程序中的情况:

  1. 定义一个路由处理程序,接受一个请求并返回EventLoopFuture<Response>
  2. 将请求主体解码为RegisterData
  3. 对提交给表单的密码进行哈希处理。
  4. 创建一个新的User,使用表单中的数据和散列的密码。
  5. 保存新的用户并解开返回的未来。
  6. 为新用户认证会话。这将在用户注册时自动登录,从而在网站注册时提供一个良好的用户体验。
  7. 返回一个重定向到主页。

接下来,在boot(routes:)下面添加以下内容 authSessionsRoutes.post("logout", use: logoutHandler)

// 1
authSessionsRoutes.get("register", use: registerHandler)
// 2
authSessionsRoutes.post("register", use: registerPostHandler)

下面是这个的作用:

  1. /registerGET请求连接到registerHandler(_:)
  2. /registerPOST请求连接到registerPostHandler(_:data:)

最后,打开base.leaf。在导航栏的结束语</ul>之前,添加以下内容:

<!-- 1 -->
#if(!userLoggedIn):
  <!-- 2 -->
  <li class="nav-item #if(title == "Register"): active #endif">
    <!-- 3 -->
    <a href="/register" class="nav-link">Register</a>
  </li>
#endif

以下是新的叶子代码的作用:

  1. 检查是否有一个登录的用户。你只想在没有用户登录的情况下显示注册链接。
  2. 在导航栏中添加一个新的导航链接。如果当前页面是注册页面,则设置active类。
  3. 添加一个链接到新的/Register路线。

保存模板,然后在Xcode中建立并运行该项目。在你的浏览器中访问http://localhost:8080。你会看到新的导航链接:

img

点击注册,你会看到新的注册页面:

img

如果你填写了表格并点击注册,该应用程序将带你到主页。注意右上方的注销按钮;这证实了注册自动登录。

基本验证

Vapor提供了一个验证模块来帮助你检查数据和模型。打开WebsiteController.swift,在底部添加以下内容:

// 1
extension RegisterData: Validatable {
  // 2
  public static func validations(
    _ validations: inout Validations
  ) {
    // 3
    validations.add("name", as: String.self, is: .ascii)
    // 4
    validations.add(
      "username", 
      as: String.self, 
      is: .alphanumeric && .count(3...))
    // 5
    validations.add(
      "password", 
      as: String.self, 
      is: .count(8...))
  }
}

下面是这个的作用:

  1. 扩展RegisterData,使其符合ValidatableValidatable允许你用Vapor来验证类型。
  2. 按照Validatable的要求实现validations(_:)
  3. 添加一个验证器,确保RegisterData的名字只包含ASCII字符,并且是一个String注意: 在对这样的名字添加限制时要小心。一些国家,例如中国,没有包含ASCII字符的名字。
  4. 添加一个验证器,以确保用户名只包含字母数字字符,并且至少有3个字符。.count(_:)需要一个Swift Range,允许你根据需要创建开放式和封闭式范围。
  5. 添加一个验证器,以确保密码至少有8个字符长。目前,不可能为两个不同的属性添加验证器。你必须提供你自己的检查,即passwordconfirmPassword匹配。

正如你所看到的,Vapor允许你对模型或传入的数据创建强大的验证。在registerPostHandler(_:)中,在方法的顶部添加以下内容:

do {
  try RegisterData.validate(content: req)
} catch {
  return req.eventLoop.future(req.redirect(to: "/register"))
}

这将调用validate(content:)RegisterData上,检查每一个你之前添加的验证器。validate(content:)可以抛出一些ValidationsError,取决于你添加的检查。在API中,你可以让这个错误传回给用户,但在网站上,这并不是一个好的用户体验。在这种情况下,你将用户重定向到"注册"页面。

建立并运行,然后在浏览器中访问"注册"页面。如果你输入的信息与验证器不匹配,应用程序就会把你送回去重试。

自定义验证

Vapor允许你编写富有表现力和复杂的验证,但有时你需要比内置选项提供的更多。例如,你可能想验证一个美国邮政编码。为了证明这一点,在WebsiteController.swift的底部,添加以下内容:

// 1
extension ValidatorResults {
  // 2
  struct ZipCode {
    let isValidZipCode: Bool
  }
}

// 3
extension ValidatorResults.ZipCode: ValidatorResult {
  // 4
  var isFailure: Bool {
    !isValidZipCode
  }

  // 5
  var successDescription: String? {
    "is a valid zip code"
  }

  // 6
  var failureDescription: String? {
    "is not a valid zip code"
  }
}

以下是新代码的作用:

  1. ValidatorResults创建一个扩展,添加你自己的结果。
  2. 创建一个`ZipCode'结果,包含结果检查。
  3. 为新的ZipCode类型创建一个扩展,符合ValidatorResult
  4. 按照ValidatorResult的要求实现isFailure。定义什么算作失败。
  5. 按照ValidatorResult的要求实现successDescription
  6. 按照ValidatorResult的要求实现failureDescription。当isFailure为真时,Vapor会在抛出一个错误时使用这个。

接下来,在文件的底部添加一个新的Validator,用于验证邮政编码:

// 1
extension Validator where T == String {
  // 2
  private static var zipCodeRegex: String {
    "^\\d{5}(?:[-\\s]\\d{4})?$"
  }

  // 3
  public static var zipCode: Validator<T> {
    // 4
    Validator { input -> ValidatorResult in
      // 5
      guard 
        let range = input.range(
          of: zipCodeRegex, 
          options: [.regularExpression]), 
        range.lowerBound == input.startIndex 
          && range.upperBound == input.endIndex
      else {
        // 6
        return ValidatorResults.ZipCode(isValidZipCode: false)
      }
      // 7
      return ValidatorResults.ZipCode(isValidZipCode: true)
    }
  }
}

以下是新验证器的作用:

  1. Validator创建一个扩展,在String上工作。
  2. 定义正则表达式,用于检查有效的美国邮政编码。
  3. 为邮编定义一个新的验证器类型。
  4. 构造一个新的Validator。这需要一个闭包,其参数是要验证的数据,并返回ValidatorResult
  5. 检查邮编是否符合正则表达式。
  6. 如果邮编不匹配,返回ValidatorResult,并将isValidZipCode设置为false
  7. 否则,返回一个成功的`ValidatorResult'。

最后,在符合RegisterDataValidatable的扩展中,在validations(_:)的末尾添加以下内容:

validations.add(
  "zipCode", 
  as: String.self, 
  is: .zipCode,
  required: false)

这是给请求正文中的zipCode属性添加一个验证。该属性必须是一个String,并与你上面添加的zipCode验证相匹配。然而,将required设置为false标志着该属性是可选的。如果它存在,Vapor将验证它,但如果没有发送zipCode,则不会出现错误。这很有用,因为你的注册表单上还没有邮政编码属性

显示一个错误

目前,当用户错误地填写表格时,应用程序会重定向到该表格,但没有显示出错的地方。打开register.leaf,在<h1>#(title)</h1>下添加如下内容:

#if(message):
  <div class="alert alert-danger" role="alert">
    Please fix the following errors:<br />
    #(message)
  </div>
#endif

如果页面上下文包括message,这将在一个新的<div>中显示它。你可以通过设置alertalert-danger类来为新消息设置适当的样式。打开WebsiteController.swift,在RegisterContext的末尾添加以下内容:

let message: String?

init(message: String? = nil) {
  self.message = message
}

这是要在注册页面上显示的信息。记住,Leaf会优雅地处理nil,允许你在正常情况下使用默认值。

registerHandler(_:)中,替换:

let context = RegisterContext()

为以下内容:

let context: RegisterContext
if let message = req.query[String.self, at: "message"] {
  context = RegisterContext(message: message)
} else {
  context = RegisterContext()
}

这将检查请求的查询。如果message存在--即URL/register?message=some-string--路由处理程序将其包含在Leaf用来渲染页面的上下文中。

最后,在registerPostHandler(_:data:)中,将catch块替换为:

catch let error as ValidationsError {
  let message = 
    error.description
    .addingPercentEncoding(
      withAllowedCharacters: .urlQueryAllowed
    ) ?? "Unknown error"
  let redirect = 
    req.redirect(to: "/register?message=\(message)")
  return req.eventLoop.future(redirect)
}

当验证失败时,路由处理程序从ValidationsError中提取descriptionVapor将所有的错误合并成一个描述。然后,代码将描述正确地转义到URL中,如果描述为nil,则提供一个默认信息。然后,它将信息添加到重定向的URL中。最后,它将用户重定向到注册页面。建立并运行,然后在你的浏览器中访问http://localhost:8080/register

提交空表格,你会看到新的信息:

img

接下来去哪?

在本章中,你学到了如何使用Vapor的验证库来检查请求的数据。你也可以将验证应用于模型和其他类型。

在下一章中,你将学习如何将TIL应用程序与OAuth提供者集成。这可以让你把登录和注册委托给在线服务,如GoogleGitHub,允许用户用现有的账户登录。