第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: "/")
}
}
下面是路线处理程序中的情况:
- 定义一个路由处理程序,接受一个请求并返回
EventLoopFuture<Response>
。 - 将请求主体解码为
RegisterData
。 - 对提交给表单的密码进行哈希处理。
- 创建一个新的
User
,使用表单中的数据和散列的密码。 - 保存新的用户并解开返回的未来。
- 为新用户认证会话。这将在用户注册时自动登录,从而在网站注册时提供一个良好的用户体验。
- 返回一个重定向到主页。
接下来,在boot(routes:)
下面添加以下内容 authSessionsRoutes.post("logout", use: logoutHandler)
:
// 1
authSessionsRoutes.get("register", use: registerHandler)
// 2
authSessionsRoutes.post("register", use: registerPostHandler)
下面是这个的作用:
- 将
/register
的GET
请求连接到registerHandler(_:)
。 - 将
/register
的POST
请求连接到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
以下是新的叶子代码的作用:
- 检查是否有一个登录的用户。你只想在没有用户登录的情况下显示注册链接。
- 在导航栏中添加一个新的导航链接。如果当前页面是注册页面,则设置
active
类。 - 添加一个链接到新的
/Register
路线。
保存模板,然后在Xcode
中建立并运行该项目。在你的浏览器中访问http://localhost:8080。你会看到新的导航链接:
点击注册,你会看到新的注册页面:
如果你填写了表格并点击注册,该应用程序将带你到主页。注意右上方的注销按钮;这证实了注册自动登录。
基本验证¶
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...))
}
}
下面是这个的作用:
- 扩展
RegisterData
,使其符合Validatable
。Validatable
允许你用Vapor来验证类型。 - 按照
Validatable
的要求实现validations(_:)
。 - 添加一个验证器,确保
RegisterData
的名字只包含ASCII
字符,并且是一个String
。注意: 在对这样的名字添加限制时要小心。一些国家,例如中国,没有包含ASCII
字符的名字。 - 添加一个验证器,以确保用户名只包含字母数字字符,并且至少有
3
个字符。.count(_:)
需要一个Swift Range
,允许你根据需要创建开放式和封闭式范围。 - 添加一个验证器,以确保密码至少有
8
个字符长。目前,不可能为两个不同的属性添加验证器。你必须提供你自己的检查,即password
和confirmPassword
匹配。
正如你所看到的,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"
}
}
以下是新代码的作用:
- 为
ValidatorResults
创建一个扩展,添加你自己的结果。 - 创建一个`ZipCode'结果,包含结果检查。
- 为新的
ZipCode
类型创建一个扩展,符合ValidatorResult
。 - 按照
ValidatorResult
的要求实现isFailure
。定义什么算作失败。 - 按照
ValidatorResult
的要求实现successDescription
。 - 按照
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)
}
}
}
以下是新验证器的作用:
- 为
Validator
创建一个扩展,在String
上工作。 - 定义正则表达式,用于检查有效的美国邮政编码。
- 为邮编定义一个新的验证器类型。
- 构造一个新的
Validator
。这需要一个闭包,其参数是要验证的数据,并返回ValidatorResult
。 - 检查邮编是否符合正则表达式。
- 如果邮编不匹配,返回
ValidatorResult
,并将isValidZipCode
设置为false
。 - 否则,返回一个成功的`ValidatorResult'。
最后,在符合RegisterData
到Validatable
的扩展中,在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>
中显示它。你可以通过设置alert
和alert-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
中提取description
。Vapor
将所有的错误合并成一个描述。然后,代码将描述正确地转义到URL
中,如果描述为nil
,则提供一个默认信息。然后,它将信息添加到重定向的URL
中。最后,它将用户重定向到注册页面。建立并运行,然后在你的浏览器中访问http://localhost:8080/register。
提交空表格,你会看到新的信息:
接下来去哪?¶
在本章中,你学到了如何使用Vapor
的验证库来检查请求的数据。你也可以将验证应用于模型和其他类型。
在下一章中,你将学习如何将TIL
应用程序与OAuth
提供者集成。这可以让你把登录和注册委托给在线服务,如Google
或GitHub
,允许用户用现有的账户登录。