第18章:API认证,第一部分¶
到目前为止,你所建立的TILApp有很多伟大的功能,但它也有一个小问题:任何人都可以创建新的用户、类别或缩写。在API或网站上没有认证,以确保只有已知的用户可以改变数据库中的内容。在本章中,你将学习如何用认证来保护你的API。你将学习如何在你的API中实现HTTP基本认证和令牌认证。你还将学习存储密码和验证用户的最佳做法。
Note
你必须在你的项目中设置和配置好PostgreSQL。如果你仍然需要这样做,请按照第6章"配置数据库"的步骤进行。
密码¶
认证是验证某人是谁的过程。这与授权不同,后者是验证用户是否有权限执行某个特定的动作。你通常用一个用户名和密码组合来验证用户,TILApp也不例外。
在Xcode中打开Vapor应用程序,打开User.swift。在User下面添加以下属性 var username: String:
@Field(key: "password")
var password: String
这个属性使用列名password来存储用户的密码。接下来,为了说明新的属性,将初始化器init(id:name:username)替换为以下内容:
init(
id: UUID? = nil,
name: String,
username: String,
password: String
) {
self.name = name
self.username = username
self.password = password
}
密码存储¶
感谢Codable,你不需要做任何额外的修改就可以创建带密码的用户。现有的UserController现在自动期望在传入的JSON中找到password属性。然而,如果不做任何改动,你将会把用户的密码保存为纯文本。
你不应该以纯文本形式存储密码。你应该始终以安全的方式存储密码。Bcrypt是散列密码的行业标准,Vapor内置了它。
Bcrypt是一种单向散列算法。这意味着你可以把密码变成哈希值,但不能把哈希值转回密码。由于Bcrypt设计得很慢,如果有人窃取了密码散列,需要很长时间来破解密码。Bcrypt将一个salt与密码混合。盐是一个独特的、随机的值,以帮助防御常见的攻击。Bcrypt还提供了一种机制,使用密码和哈希值来验证密码。
打开UsersController.swift,找到createHandler(_:user:)并在let user = try req.content.decode(User.self)后添加如下内容:
user.password = try Bcrypt.hash(user.password)
在将用户的密码保存在数据库中之前,这将对其进行加密。
让用户名独一无二¶
在本章接下来的章节中,你将会使用用户名和密码来唯一地识别用户。目前,没有什么可以防止多个用户拥有相同的用户名。
打开CreateUser.swift。在.create()之前添加:
.field("password", .string, .required)
.unique(on: "username")
这就更新了迁移,为密码增加了一个字段,并为User的username增加了一个唯一索引。在应用程序运行更新后的迁移后,任何试图创建重复用户名的行为都会导致错误。
修复测试¶
你改变了User的初始化器,所以你需要更新测试,以便Xcode可以编译你的应用程序。打开UserTests.swift,在testUserCanBeSavedWithAPI()中,将let user = User...改为以下内容:
let user = User(
name: usersName,
username: usersUsername,
password: "password")
接下来,打开Models+Testable.swift,更新User的扩展中的create(name:username:on:)。再次,为password参数添加一个值:
let user = User(
name: name,
username: username,
password: "password")
从API中返回用户¶
由于模型已经改变,你需要重置数据库。Fluent已经运行了User迁移,但是表现在有一个新的列。为了在表中添加新的列,你必须删除数据库,这样Fluent才会再次运行迁移。在终端,输入:
# 1
docker stop postgres
# 2
docker rm postgres
# 3
docker run --name postgres -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
下面是这个的作用:
- 停止运行中的
Docker容器postgres。这是当前运行数据库的容器。 - 移除
Docker容器postgres,以删除任何现有的数据。 - 启动一个新的运行
PostgreSQL的Docker容器。更多信息请参见第6章"配置数据库"。
现在,构建并运行,Fluent会用你的新添加的内容创建一个干净的数据库。
启动RESTed,创建一个新的请求,并进行如下配置:
- URL: http://localhost:8080/api/users/
- method: POST
- Parameter encoding: JSON-encoded
添加三个带有名称和值的参数:
- name: your name
- username: a username of your choice
- password: a password of your choice
点击Send Request。你的应用程序创建了请求的用户,但响应返回了密码哈希:

这可不好! 你应该保护密码哈希值,永远不要在响应中返回它们。事实上,任何由API返回的用户都包括密码哈希值,包括列出所有的用户! 这是因为你在所有的路由中返回User。你应该返回一个User的"公共视图"。
在Xcode中,打开User.swift,在User的初始化器下面添加以下内容:
final class Public: Content {
var id: UUID?
var name: String
var username: String
init(id: UUID?, name: String, username: String) {
self.id = id
self.name = name
self.username = username
}
}
这就创建了一个内层类来代表User的公共视图,以便在响应中返回。接下来,在User.swift的底部添加以下内容:
extension User {
// 1
func convertToPublic() -> User.Public {
// 2
return User.Public(id: id, name: name, username: username)
}
}
以下是新方法的作用:
- 在
User上定义一个方法,返回User.Public。 - 创建一个当前对象的公共版本。
最后,在新的扩展下面添加以下内容:
// 1
extension EventLoopFuture where Value: User {
// 2
func convertToPublic() -> EventLoopFuture<User.Public> {
// 3
return self.map { user in
// 4
return user.convertToPublic()
}
}
}
// 5
extension Collection where Element: User {
// 6
func convertToPublic() -> [User.Public] {
// 7
return self.map { $0.convertToPublic() }
}
}
// 8
extension EventLoopFuture where Value == Array<User> {
// 9
func convertToPublic() -> EventLoopFuture<[User.Public]> {
// 10
return self.map { $0.convertToPublic() }
}
}
下面是这个的作用:
- 定义
EventLoopFuture<User>的扩展。 - 定义一个新的方法,返回一个
EventLoopFuture<User.Public>。 - 解除包含在
self中的用户。 - 将
User对象转换为User.Public。 - 为
[User]定义一个扩展。 - 定义一个新方法,返回
[User.Public]。 - 将数组中所有的
User对象转换为User.Public。 - 定义一个
EventLoopFuture<[User]>的扩展。 - 定义一个新的方法,返回
EventLoopFuture<[User.Public]>。 - 解除包含在·中的数组,并使用之前的扩展将所有的
User转换为User.Public。
这些扩展允许你在EventLoopFuture<User>、[User]和EventLoopFuture<[User]>上调用convertToPublic()。这有助于理顺你的代码,减少嵌套。这些新方法允许你改变你的路由处理程序以返回公共用户。
首先,打开UsersController.swift,改变createHandler(_:user:)的返回类型:
func createHandler(_ req: Request)
-> EventLoopFuture<User.Public> {
接下来,改变map的结果,以返回一个公共用户:
return user.save(on: req.db).map { user.convertToPublic() }
这使用新的方法将一个User转换为User.Public。建立并运行,然后在RESTed中创建一个新用户。你会发现用户的密码哈希值不再被返回:

现在,你必须更新其余返回User的路由。
首先,在UsersController.swift中,将getAllHandler(_:)的签名改为如下:
func getAllHandler(_ req: Request)
-> EventLoopFuture<[User.Public]> {
接下来,将getAllHandler(_:)的主体改为如下:
User.query(on: req.db).all().convertToPublic()
这使用了EventLoopFuture<[User]>的扩展,将从数据库返回的用户转换为User.Public。接下来,改变getHandler(_:)的签名以返回一个公共用户:
func getHandler(_ req: Request)
-> EventLoopFuture<User.Public> {
接下来,改变主体以返回一个公共用户:
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.convertToPublic()
最后,打开AcronymsController.swift,替换getUserHandler(_:),使其返回一个公共用户:
// 1
func getUserHandler(_ req: Request)
-> EventLoopFuture<User.Public> {
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 2
acronym.$user.get(on: req.db).convertToPublic()
}
}
以下是变化的内容:
- 将该方法的返回类型改为
Future<User.Public>。 - 对缩写的用户调用
convertToPublic()以返回一个公共用户。
现在,对你的API进行检索用户的调用都不会返回一个密码哈希值。
基本认证¶
HTTP基本认证是一种通过HTTP发送证书的标准化方法,由RFC 7617定义。你通常在HTTP请求的授权标头中包含凭证。
为了生成这个头的令牌,你把用户名和密码结合起来,然后对结果进行Base64编码。
例如,对于用户名timc和密码password,合并后的凭证字符串是:
timc:password
然后你对其进行Base64编码,这样就可以得到:
dGltYzpwYXNzd29yZA==
完整的标题变成了:
Authorization: Basic dGltYzpwYXNzd29yZA==
认证是内置在Vapor中的,包含了使用HTTP Basic认证的帮助器。打开User.swift,在文件的底部,添加以下内容:
// 1
extension User: ModelAuthenticatable {
// 2
static let usernameKey = \User.$username
// 3
static let passwordHashKey = \User.$password
// 4
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.password)
}
}
下面是这个的作用:
- 使
User符合ModelAuthenticatable。这是一个允许Fluent Model使用HTTP基本认证的协议。 - 告诉
Vapor User的哪个关键路径是用户名。 - 告诉
Vapor User的关键路径是密码散列。 - 按照
ModelAuthenticatable的要求实现verify(password:)。由于你使用Bcrypt对User的密码进行散列,在这里用Bcrypt验证散列。
打开AcronymsController.swift,在boot(route:)的底部添加以下内容:
// 1
let basicAuthMiddleware = User.authenticator()
// 2
let guardAuthMiddleware = User.guardMiddleware()
// 3
let protected = acronymsRoutes.grouped(
basicAuthMiddleware,
guardAuthMiddleware)
// 4
protected.post(use: createHandler)
下面是这个的作用:
- 创建一个
ModelAuthenticator中间件的实例,它使用HTTP基本认证。由于User符合ModelAuthenticatable,这可以作为模型的一个静态方法。 - 创建一个
GuardAuthenticationMiddleware的实例,确保请求包含认证的用户。 - 创建一个中间件组,使用
basicAuthMiddleware和guardAuthMiddleware。 - 通过这个中间件组将"创建首字母缩写"路径连接到
createHandler(_:acronym:)。
中间件允许你在你的应用程序中拦截请求和响应。在这个例子中,basicAuthMiddleware拦截了请求并验证了所提供的用户。你可以把中间件连锁起来。在上面的例子中,basicAuthMiddleware认证了用户。然后guardAuthMiddleware确保该请求包含一个认证的用户。如果没有认证的用户,guardAuthMiddleware会抛出一个错误。你可以在第29章"中间件"中进一步了解中间件。
这确保只有使用HTTP基本认证的请求才能创建缩略语。
接下来,删除以下内容,以删除未经认证的路由:
acronymsRoutes.post(use: createHandler)
建立并运行,然后启动RESTed。创建一个新的请求,并进行如下配置:
- URL: http://localhost:8080/api/acronyms
- method: POST
- Parameter encoding: JSON-encoded
添加三个带有名称和值的参数:
- short: OMG
- long: Oh My God
- userID: The ID of the user created earlier
点击Send Request,你会收到一个401 Unauthorized的错误响应。你应该看到以下内容:

在RESTed中,点击Authorization,输入之前创建的用户的用户名和密码。勾选在认证挑战前呈现,然后点击OK:

这就设置了上述的基本Authorization头。再次点击Send Request。这一次,请求成功了:

Token认证¶
获得一个Token¶
在这个阶段,只有经过认证的用户可以创建首字母缩写。然而,所有其他的破坏性路由仍然是不受保护的。要求用户在每次请求时都输入凭证是不现实的。你也不想在你的应用程序中的任何地方存储用户的密码,因为你必须以纯文本形式存储它。相反,你将允许用户登录到你的API。当他们登录时,你将他们的凭证换成客户端可以保存的Token。
在Sources/App/Models创建一个新文件,Token.swift。打开这个新文件,并添加以下内容:
import Vapor
import Fluent
final class Token: Model, Content {
static let schema = "tokens"
@ID
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "userID")
var user: User
init() {}
init(id: UUID? = nil, value: String, userID: User.IDValue) {
self.id = id
self.value = value
self.$user.id = userID
}
}
这为Token定义了一个模型,包含以下属性:
id:模型的ID。value:提供给客户的token字符串。user:一个@Parent字段,指向token所有者的用户。
在Sources/App/Migrations中为新模型创建一个迁移文件,CreateToken.swift,并在下面插入迁移:
import Fluent
struct CreateToken: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens")
.id()
.field("value", .string, .required)
.field(
"userID",
.uuid,
.required,
.references("users", "id", onDelete: .cascade))
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("tokens").delete()
}
}
和之前的其他迁移一样,这个迁移为Token创建了一个表。它还为userID字段创建了对User的引用。这个引用被标记为级联删除,这样当你删除一个用户时,任何Token都会被自动删除。在configure.swift中,在app.migrations.add(CreateAcronymCategoryPivot())之后添加以下内容:
app.migrations.add(CreateToken())
这样就把CreateToken``添加到了迁移列表中,这样Vapor就会在应用程序下次启动时创建该表。当一个用户登录时,应用程序必须为该用户创建一个token。
打开Token.swift,在文件的底部添加以下内容:
extension Token {
// 1
static func generate(for user: User) throws -> Token {
// 2
let random = [UInt8].random(count: 16).base64
// 3
return try Token(value: random, userID: user.requireID())
}
}
以下是这个扩展的作用:
- 定义一个静态方法来为用户生成一个
Token。 - 生成
16个随机字节作为Token,并对其进行Base64编码。 - 使用随机字节的
Base64编码表示和用户的ID创建一个Token。
打开UsersController.swift,在getAcronymsHandler(_:)下添加以下内容:
// 1
func loginHandler(_ req: Request) throws
-> EventLoopFuture<Token> {
// 2
let user = try req.auth.require(User.self)
// 3
let token = try Token.generate(for: user)
// 4
return token.save(on: req.db).map { token }
}
下面是这个的作用:
- 定义一个用于登录用户的路由处理程序。
- 从请求中获取认证的用户。你将用
HTTP基本认证中间件来保护这个路由。这将把用户的身份保存在请求的认证缓存中,允许你以后检索用户对象。req.auth.require(_:)如果没有认证的用户,会抛出一个认证错误。 - 为用户创建一个
token。 - 保存并返回
token。
在boot(rouse:)的底部添加以下内容:
// 1
let basicAuthMiddleware = User.authenticator()
let basicAuthGroup = usersRoute.grouped(basicAuthMiddleware)
// 2
basicAuthGroup.post("login", use: loginHandler)
下面是这个的作用:
- 使用
HTTP基本认证创建一个受保护的路由组,就像你创建一个缩写一样。这没有使用GuardAuthenticationMiddleware,因为req.auth.require(_:)在用户没有被认证时抛出正确的错误。 - 通过受保护的组连接
/api/users/login到loginHandler(_:)。
建立并运行,然后回到RESTed。
确保你已经配置了HTTP基本认证,并将URL设置为http://localhost:8080/api/users/login。
点击Send Request,你会收到一个token的回馈:

使用token¶
打开Token.swift,在文件的末尾添加以下内容:
// 1
extension Token: ModelTokenAuthenticatable {
// 2
static let valueKey = \Token.$value
// 3
static let userKey = \Token.$user
// 4
typealias User = App.User
// 5
var isValid: Bool {
true
}
}
下面是这个的作用:
- 使
Token符合Vapor的ModelTokenAuthenticatable协议。这允许你将令牌用于HTTP承载认证。 - 告诉
Vapor价值键的关键路径,在这种情况下,Token的value预测值。 - 告诉
Vapor用户键的路径,在此情况下,Token的user预测值。 - 告诉
Vapor该用户是什么类型。 - 确定
token是否有效。现在返回true,但你可以在将来添加一个到期日或撤销的属性来检查。
Bearer authentication是一种发送token以验证请求的机制。它使用Authorization头,就像HTTP基本认证一样,但头看起来像Authorization:Bearer <TOKEN STRING>。
目前,当用户创建首字母缩写时,他们必须在请求中发送他们的ID。然而,由于你要求认证,你现在知道每个请求是哪个用户发送的。在AcronymsController.swift中,从CreateAcronymData中删除let userID: UUID。接下来,在createHandler(_:)中,替换:
let acronym = Acronym(
short: data.short,
long: data.long,
userID: data.userID)
为以下内容:
// 1
let user = try req.auth.require(User.self)
// 2
let acronym = try Acronym(
short: data.short,
long: data.long,
userID: user.requireID())
所做的改变是:
- 从请求中获取认证的用户。
- 使用来自请求和认证用户的数据创建一个新的
Acronym。
接下来,将updateHandler(_:)替换为以下内容:
func updateHandler(_ req: Request) throws
-> EventLoopFuture<Acronym> {
let updateData =
try req.content.decode(CreateAcronymData.self)
// 1
let user = try req.auth.require(User.self)
// 2
let userID = try user.requireID()
return Acronym
.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
acronym.short = updateData.short
acronym.long = updateData.long
// 3
acronym.$user.id = userID
return acronym.save(on: req.db).map {
acronym
}
}
}
所做的改变是:
- 从请求中获取认证的用户。
- 从用户中获取用户
ID。在这里做这个很有用,因为你不能扔在flatMap(_:)里面。 - 将缩写的用户
ID设置为上面步骤中的用户ID。
最后,更新测试,使项目可以编译。打开AcronymTests.swift。在testAcronymCanBeSavedWithAPI()中,将let createAcronymData = ...改为以下内容:
let createAcronymData =
CreateAcronymData(short: acronymShort, long: acronymLong)
这样就删除了userID参数,因为它不再需要了。同时,删除let user = try User.create...一行,因为它不再需要了。最后,在testUpdatingAnAcronym()中,将let updatedAcronymData = ...改为以下内容,以删除额外的userID参数:
let updatedAcronymData =
CreateAcronymData(short: acronymShort, long: newLong)
返回到AcronymsController.swift。在boot(routes:)中,删除你之前用来保护"创建首字母缩写"路由的代码,用下面的内容代替它:
// 1
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
// 2
let tokenAuthGroup = acronymsRoutes.grouped(
tokenAuthMiddleware,
guardAuthMiddleware)
// 3
tokenAuthGroup.post(use: createHandler)
以下是新代码的作用:
- 为
Token创建一个ModelTokenAuthenticator中间件。这将从请求中提取出承载token,并将其转换为登录用户。 - 使用
tokenAuthMiddleware和guardAuthMiddleware创建一个路由组,以保护用token认证创建缩写的路由。 - 通过这个中间件组,使用新的
AcronymCreateData将"创建首字母缩写"路径连接到createHandler(_:data:)。
建立并运行,然后回到RESTed。复制从用户登录返回的token值字符串。像这样配置一个请求:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
添加两个带有名称和值的参数:
- short: IKR
- long: I Know Right
为Authorization创建一个新的头域,其值为Bearer <TOKEN STRING>,使用你先前复制的token字符串。删除你用于登录的HTTP基本认证凭证。
要做到这一点,请点击Authorization,删除用户名和密码,并取消勾选在认证挑战前呈现。
点击Send Request,你会看到创建的首字母缩写返回:

打开AcronymsController.swift,找到boot(routes:),并删除以下几行:
acronymsRoutes.put(":acronymID", use: updateHandler)
acronymsRoutes.delete(":acronymID", use: deleteHandler)
acronymsRoutes.post(":acronymID", "categories", ":categoryID",
use: addCategoriesHandler)
acronymsRoutes.delete(":acronymID", "categories", ":categoryID",
use: removeCategoriesHandler)
这是所有不是get()路由的原始路由。在boot(routes:)的底部,添加它们的替换:
tokenAuthGroup.delete(":acronymID", use: deleteHandler)
tokenAuthGroup.put(":acronymID", use: updateHandler)
tokenAuthGroup.post(
":acronymID",
"categories",
":categoryID",
use: addCategoriesHandler)
tokenAuthGroup.delete(
":acronymID",
"categories",
":categoryID",
use: removeCategoriesHandler)
这确保只有经过认证的用户可以创建、编辑和删除缩写,并为缩写添加类别。未认证的用户仍然可以查看首字母缩写的细节。
现在,打开CategoriesController.swift,在boot(routes:)中,删除categoriesRoute.post(use: createHandler)。
在该方法的结尾处用以下内容代替:
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = categoriesRoute.grouped(
tokenAuthMiddleware,
guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)
这使用了token中间件来保护类别的创建,就像创建一个缩写一样,确保只有经过认证的用户才能创建类别。最后,打开UsersController.swift,删除usersRoute.post(use: createHandler)。在boot(route:)的底部,添加以下内容:
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = usersRoute.grouped(
tokenAuthMiddleware,
guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)
同样,使用tokenAuthMiddleware和guardAuthMiddleware确保只有经过认证的用户才能创建其他用户。这可以防止任何人创建一个用户来向你刚刚保护的路由发送请求!
现在,所有可以执行"破坏性"操作的API路由--也就是创建、编辑或删除资源--都受到了保护。对于这些操作,应用程序只接受来自认证用户的请求。
数据库播种¶
在这一点上,API是安全的,但现在还有一个问题。当你部署你的应用程序,或者下一次恢复数据库的时候,你的数据库中就没有任何用户了。
但是,你不能创建一个新的用户,因为那条路由需要认证! 解决这个问题的一个方法是,在应用程序第一次启动时,对数据库进行播种并创建一个用户。在Vapor中,你可以通过迁移来做到这一点。
在Sources/App/Migrations中创建一个新文件,CreateAdminUser.swift。打开这个新文件,添加以下内容:
import Fluent
import Vapor
// 1
struct CreateAdminUser: Migration {
// 2
func prepare(on database: Database) -> EventLoopFuture<Void> {
// 3
let passwordHash: String
do {
passwordHash = try Bcrypt.hash("password")
} catch {
return database.eventLoop.future(error: error)
}
// 4
let user = User(
name: "Admin",
username: "admin",
password: passwordHash)
// 5
return user.save(on: database)
}
// 6
func revert(on database: Database) -> EventLoopFuture<Void> {
// 7
User.query(on: database)
.filter(\.$username == "admin")
.delete()
}
}
下面是这个的作用:
- 定义一个符合
Migration的新类型。 - 实现所需的
prepare(on:)。 - 从密码中创建一个密码哈希值。捕获任何抛出的错误并返回一个失败的未来。
- 创建一个新的用户,名字是
Admin,用户名是admin,密码是哈希值。 - 保存该用户并返回。
- 执行所需的
revert(on:)。 - 查询
User并删除所有用户名与admin相匹配的行。因为用户名必须是唯一的,所以这只删除了一个管理员行。
Note
显然,在一个生产系统中,你不应该使用password作为你的管理用户的密码!你也不希望硬编码,以防它在源码控制中出现。你也不希望硬编码密码,以防它最终出现在源代码控制中。你可以读取一个环境变量或者生成一个随机密码并打印出来。
打开configure.swift,在app.migrations.add(CreateToken())之后添加以下内容:
app.migrations.add(CreateAdminUser())
这将把CreateAdminUser添加到迁移列表中,以便应用程序在下次启动时执行迁移。
建立并运行。前往RESTed,试用所有新保护的路由。你甚至可以用新的管理员用户来登录。
接下来去哪?¶
在本章中,你了解了HTTP基本认证和承载认证。你看到了认证中间件是如何简化你的代码并为你做很多繁重的工作的。你看到了如何修改你现有的模型,使之与Vapor的认证功能一起工作。你把这一切粘在一起,把认证添加到你的API。
但是,还有很多事情要做。翻过这一页,忙着更新你的测试套件和你的iOS应用,使其与新的认证功能一起工作。