第22章:谷歌认证¶
在前几章中,你学会了如何在TIL
网站上添加认证。然而,有时用户并不想为一个应用程序创建额外的账户,而是希望使用他们现有的账户。
在这一章中,你将学习如何使用OAuth 2.0
将认证委托给Google
,这样用户就可以用他们的Google
账户登录了。
OAuth 2.0
¶
OAuth 2.0是一个授权框架,允许第三方应用程序代表用户访问资源。每当你用你的谷歌账户登录到一个网站,你就在使用OAuth
。
当你点击用Login with Google
时,谷歌是认证你的网站。然后,你授权应用程序可以访问你的谷歌数据,如你的电子邮件。一旦你允许应用程序访问,谷歌会给应用程序一个令牌。该应用程序使用这个令牌来验证对谷歌API
的请求。你将在本章中实现这一技术。
Note
你必须有一个谷歌账户来完成这一章。如果你没有,请访问https://accounts.google.com/SignUp,创建一个。
Imperial
¶
编写所有必要的脚手架来与Google
的OAuth
系统交互并获得一个令牌是一件很耗时的工作!有一个叫做Imperial
的社区包,它可以完成繁重的工作。
有一个叫做Imperial
的社区包,https://github.com/vapor-community/Imperial,它为你完成了繁重的工作。它有谷歌、Facebook
和GitHub
的集成,还有一些其他的。
添加到你的项目中¶
在Xcode
中打开Package.swift
,添加新的依赖关系。替换:
.package(
url: "https://github.com/vapor/leaf.git",
from: "4.0.0")
为以下内容:
.package(
url: "https://github.com/vapor/leaf.git",
from: "4.0.0"),
.package(
url: "https://github.com/vapor-community/Imperial.git",
from: "1.0.0")
接下来,将该依赖关系添加到你的App
目标的依赖关系数组中。替换:
.product(name: "Leaf", package: "leaf")
为以下内容:
.product(name: "Leaf", package: "leaf"),
.product(name: "ImperialGoogle", package: "Imperial")
接下来,为一个新的控制器创建一个文件来管理Imperial
的路线。在Sources/App/Controllers
中创建一个名为ImperialController.swift
的文件。打开这个新文件,创建一个新的空控制器:
import ImperialGoogle
import Vapor
import Fluent
struct ImperialController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
}
}
这将创建一个新的类型,ImperialController
,它符合RouteCollection
,实现所需的boot(routes:)
。
最后,打开routes.swift
,在routes(_:)
的底部添加控制器到你的应用程序。
let imperialController = ImperialController()
try app.register(collection: imperialController)
在谷歌设置你的应用程序¶
为了能够在你的应用程序中使用Google OAuth
,你必须首先向Google
注册应用程序。在你的浏览器中,进入https://console.developers.google.com/apis/credentials。
如果这是你第一次使用谷歌的凭证,网站会提示你创建一个项目:
点击Create Project
,为TIL
应用程序创建一个项目。在表格中填写一个适当的名称,例如Vapor TIL
:
创建项目后,该网站会带你回到新创建项目的谷歌凭证页面。这一次,点击Create Credentials
,为TIL
应用创建凭证,并选择OAuth client ID
:
接下来,点击Configure consent screen
来设置谷歌呈现给用户的页面,这样他们可以允许你的应用程序访问他们的详细信息。
为用户类型选择External
,然后点击Create
。
添加一个应用程序名称并选择用户支持电子邮件。
在页面的底部,添加你的开发者联系信息。点击Save and Continue
。
在下一个屏幕上,你为你的应用程序配置作用域。这些是你想向用户请求的权限,比如他们的电子邮件地址。点击Add or remove scopes
,选择/auth/userinfo.email
和/auth/userinfo.profile
。这样你就可以访问用户的电子邮件和个人资料,你需要在TIL
应用程序中创建一个账户。
一旦你选择了范围,点击Update
,然后Save and continue
。接下来,你需要选择你将用于测试的用户。点击Save and continue
,添加任何你希望能够登录的用户。如果你发布了你的应用程序,你可以验证你的域名和应用程序以消除这一限制。点击Save and continue
。
你已经完成了OAuth
同意界面,所以点击Back to dashboard
。再次点击Credentials
页面,再次点击Create Credentials
,选择OAuth
客户端ID
。创建客户端ID
时,选择Web application
。为你的应用程序添加一个重定向URI进行测试 - http://localhost:8080/oauth/google。这是用户允许你的应用程序访问他们的数据后,谷歌重定向回的URL
。
如果你想把你的应用程序部署到互联网上,例如用AWS
或Heroku
,为该网站的URL
添加另一个重定向--例如,https://rw-til-vapor.herokuapp.com/oauth/google:
点击Create
,网站会给你提供你的客户ID
和客户秘密:
Note
你必须保持这些安全和可靠。你的秘密允许你访问谷歌的API
,你不应该分享或检查秘密到源代码控制。你应该把它当作一个密码。
设置集成¶
现在你已经在Google
注册了你的应用程序,你可以开始整合Imperial
。打开ImperialController.swift
,在boot(routes:)
下添加以下内容:
func processGoogleLogin(request: Request, token: String)
throws -> EventLoopFuture<ResponseEncodable> {
request.eventLoop.future(request.redirect(to: "/"))
}
这定义了一个处理谷歌登录的方法。这个处理方法只是将用户重定向到主页--与常规登录的方式相同。Imperial
使用这个方法作为处理Google
重定向后的最终回调。注意使用eventLoop.future(_:)
从request.redirect(to:)
创建一个未来。这是因为Imperial
使用的方法需要一个EventLoopFuture
。
接下来,通过在boot(routes:)
中添加以下内容来设置Imperial
路由:
guard let googleCallbackURL =
Environment.get("GOOGLE_CALLBACK_URL") else {
fatalError("Google callback URL not set")
}
try routes.oAuth(
from: Google.self,
authenticate: "login-google",
callback: googleCallbackURL,
scope: ["profile", "email"],
completion: processGoogleLogin)
下面是这个的作用:
- 从环境变量中获取
Google
的回调URL - 这是你在Google
控制台中设置的URL。 - 将
Imperial
的Google OAuth
路由器与你的应用程序的路由器注册。 - 告诉
Imperial
使用谷歌的处理程序。 - 将
/login-google
路由设置为触发OAuth
流程的路由。这是应用程序用来允许用户通过谷歌登录的路由。 - 提供回调
URL
给Imperial
。 - 从
Google
请求profile
和email
作用域--这与你之前创建应用程序时设置的作用域一致。 - 将完成处理程序设置为
processGoogleLogin(request:token:)
--你在上面创建的方法。
为了让Imperial
工作,你需要向它提供谷歌给你的客户ID
和客户秘密。你用环境变量把这些提供给Imperial
。有许多方法可以做到这一点,但Vapor
已经内置了对.env
文件的支持。这允许你在一个文件中定义环境变量,让Vapor
读取。这在命令行和Xcode
中都适用。
Note
.env
文件依赖于你在Xcode
中运行时设置的自定义工作目录。如果你需要更多关于如何做的信息,请参阅第14章,"用Leaf
制作模板"。在你的项目目录下创建一个名为.env
的新文件,并在你喜欢的文本编辑器中打开它。插入以下内容。
GOOGLE_CALLBACK_URL=http://localhost:8080/oauth/google
GOOGLE_CLIENT_ID=<THE_CLIENT_ID_FROM_GOOGLE>
GOOGLE_CLIENT_SECRET=<THE_CLIENT_SECRET_FROM_GOOGLE>
插入你的客户ID
和谷歌提供的客户秘密。
Note
将.env
文件添加到.gitignore
中是一个很好的做法,这样你就不会将秘密检查到源代码控制中。
与网络认证整合¶
为用户提供一个无缝的体验,并与常规登录的体验相匹配,这一点很重要。要做到这一点,当用户第一次用谷歌登录时,你需要创建一个新用户。为了创建一个用户,你可以使用谷歌的API
,使用OAuth
令牌获得必要的细节。
向第三方API
发送请求¶
在ImperialController.swift
的底部,添加一个新类型来解码来自Google
的API
的数据:
struct GoogleUserInfo: Content {
let email: String
let name: String
}
对谷歌API的请求会返回许多字段。然而,你只关心电子邮件,它成为用户名,以及名字。
接下来,在GoogleUserInfo
下,添加以下内容:
extension Google {
// 1
static func getUser(on request: Request)
throws -> EventLoopFuture<GoogleUserInfo> {
// 2
var headers = HTTPHeaders()
headers.bearerAuthorization =
try BearerAuthorization(token: request.accessToken())
// 3
let googleAPIURL: URI =
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json"
// 4
return request
.client
.get(googleAPIURL, headers: headers)
.flatMapThrowing { response in
// 5
guard response.status == .ok else {
// 6
if response.status == .unauthorized {
throw Abort.redirect(to: "/login-google")
} else {
throw Abort(.internalServerError)
}
}
// 7
return try response.content
.decode(GoogleUserInfo.self)
}
}
}
下面是这个的作用:
- 为
Imperial
的Google
服务添加一个新的方法,从Google API
中获取用户的详细信息。 - 通过在授权头中添加
OAuth
令牌来设置请求的头信息。 - 设置请求的
URL
--这是谷歌的API
,用于获取用户的信息。这使用了Vapor
的URI
类型,Client
需要这个类型。 - 使用
request.client
向Google
发送请求。get()
向提供的URL
发送HTTP GET
请求。解除返回的未来响应。 - 确保响应状态是
200 OK
。 - 否则,如果响应是
401 Unauthorized
,则返回到登录页面,或者返回一个错误。 - 将响应中的数据解码到
GoogleUserInfo
,并返回结果。
接下来,将processGoogleLogin(request:token:)
的内容替换为以下内容:
// 1
try Google
.getUser(on: request)
.flatMap { userInfo in
// 2
User
.query(on: request.db)
.filter(\.$username == userInfo.email)
.first()
.flatMap { foundUser in
guard let existingUser = foundUser else {
// 3
let user = User(
name: userInfo.name,
username: userInfo.email,
password: UUID().uuidString)
// 4
return user
.save(on: request.db)
.map {
// 5
request.session.authenticate(user)
return request.redirect(to: "/")
}
}
// 6
request.session.authenticate(existingUser)
return request.eventLoop
.future(request.redirect(to: "/"))
}
}
以下是新代码的作用:
- 从谷歌获取用户信息。
- 通过查找电子邮件作为用户名,查看该用户是否存在于数据库中。
- 如果用户不存在,用
Google
提供的用户信息中的姓名和电子邮件创建一个新的User
。将密码设置为UUID
字符串,因为你不需要它。这可以确保没有人可以通过正常的密码登录到这个账户。 - 保存用户并解开返回的未来。
- 调用
session.authenticate(_:)
将创建的用户保存在会话中,以便网站允许访问。重定向回到主页。 - 如果用户已经存在,在会话中验证用户并重定向到主页。
Note
在实际应用中,你可能要考虑使用一个标志来区分在你的网站上注册的用户和用OAuth
登录的用户。
最后要做的是在网站上添加一个按钮,让用户利用新的功能! 打开login.leaf
,在</form>
下,添加以下内容:
<a href="/login-google">
<img class="mt-3" src="/images/sign-in-with-google.png"
alt="Sign In With Google">
</a>
本章的示例项目包含一个新的、由Google
提供的图片,sign-in-with-google.png
,用于显示Sign in with Google
按钮。这将图像添加为/login-google
的链接--提供给Imperial
开始登录的路线。
保存Leaf
模板并在Xcode
中建立和运行应用程序。记得在运行前设置自定义工作目录。在你的浏览器中访问http://localhost:8080。
点击Create An Acronym
,应用程序会带你到登录页面。你会看到新的Sign in with Google
按钮:
点击新按钮,应用程序会带你到一个谷歌页面,允许TIL
应用程序访问你的信息:
选择你要使用的账户,应用程序会将你重新引导到主页。进入所有用户屏幕,你会看到你的新用户账户。如果你创建了一个缩写,应用程序也会使用该新用户。
与iOS
集成¶
你已经将Imperial
与TIL
网站整合,允许用户用Google
登录。然而,你还有另一个客户端--iOS
应用程序。你可以重用大部分现有的代码,让用户也能用Google
登录到iOS
应用中! 在ImperialController.swift
中,在processGoogleLogin(_:)
下面添加一个新的路由处理程序:
func iOSGoogleLogin(_ req: Request) -> Response {
// 1
req.session.data["oauth_login"] = "iOS"
// 2
return req.redirect(to: "/login-google")
}
以下是新路线的作用:
- 在请求的会话中添加一个条目,指出这个
OAuth
登录尝试来自iOS
。 - 重定向到你之前创建的
URL
,开始使用谷歌登录网站的OAuth
流程。
在boot(routes:)
的底部注册新的路由:
routes.get("iOS", "login-google", use: iOSGoogleLogin)
这就把对/iOS/login-google
的GET
请求路由到iOSGoogleLogin(_:)'。然后,在
iOSGoogleLogin(_:)`下面,添加一个新方法来创建登录的重定向:
// 1
func generateRedirect(on req: Request, for user: User)
-> EventLoopFuture<ResponseEncodable> {
let redirectURL: EventLoopFuture<String>
// 2
if req.session.data["oauth_login"] == "iOS" {
do {
// 3
let token = try Token.generate(for: user)
// 4
redirectURL = token.save(on: req.db).map {
"tilapp://auth?token=\(token.value)"
}
// 5
} catch {
return req.eventLoop.future(error: error)
}
} else {
// 6
redirectURL = req.eventLoop.future("/")
}
// 7
req.session.data["oauth_login"] = nil
// 8
return redirectURL.map { url in
req.redirect(to: url)
}
}
以下是新代码的作用:
- 定义一个新的方法,同时接受
Request
和User
来生成一个重定向。这个新方法返回EventLoopFuture<ResponseEncodable>
。 - 检查请求的会话数据中的
oauth_login
标志,看它是否与iOSGoogleLogin(_:)
中设置的标志相符。 - 如果请求来自
iOS
,为用户生成一个令牌。 - 保存令牌,解析返回的未来,并返回一个重定向。这使用
tilapp
方案,并返回令牌作为查询参数。你将在iOS
应用程序中使用这个。 - 捕捉生成令牌时抛出的任何错误,并返回一个失败的未来。
- 如果请求不是来自
iOS
,为原始重定向URL
创建一个未来字符串。 - 为下一个会话重置
oauth_login
标志。 - 解析
future
并使用返回的字符串返回重定向。
接下来,在processGoogleLogin(request:token:)
中,替换:
return user.save(on: request.db).map {
request.session.authenticate(user)
return request.redirect(to: "/")
}
为以下内容:
return user.save(on: request.db).flatMap {
request.session.authenticate(user)
return generateRedirect(on: request, for: user)
}
这将为新用户返回一个生成的重定向,而不是硬编码的/
。它还用flatMap
替换了map
,因为闭包现在返回一个future
。
最后,替换:
return request.eventLoop
.future(request.redirect(to: "/"))
为以下内容:
return generateRedirect(on: request, for: existingUser)
这将返回一个为现有用户生成的重定向。构建并运行该应用,然后打开TILiOS
的启动项目。这个iOS
项目与第19章"API
认证,第二部分"中的最终项目类似。现在的登录屏幕包含一个新的按钮,用于用Google
登录。
打开LoginTableViewController.swift
。点击Sign in with Google
按钮时,会触发signInWithGoogleButtonTapped(_:)
。这在目前没有任何作用。在文件的顶部,在import UIKit
下面添加:
import AuthenticationServices
这将导入认证服务框架,你将使用它来登录。然后,在signInWithGoogleButtonTapped(_:)
中,添加以下内容:
// 1
guard let googleAuthURL = URL(
string: "http://localhost:8080/iOS/login-google")
else {
return
}
// 2
let scheme = "tilapp"
// 3
let session = ASWebAuthenticationSession(
url: googleAuthURL,
callbackURLScheme: scheme) { callbackURL, error in
}
下面是这个的作用:
- 创建一个
URL
,与你之前在TILApp
中为登录谷歌而创建的路线相匹配。 - 定义要使用的方案。这与你之前设置的
TILApp
的重定向方案相匹配。 - 创建一个
ASWebAuthenticationSession
的实例。这允许用户使用Safari
浏览器的现有凭证来验证TIL
应用程序。
接下来,在文件的底部,添加以下扩展:
extension LoginTableViewController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(
for session: ASWebAuthenticationSession
) -> ASPresentationAnchor {
guard let window = view.window else {
fatalError("No window found in view")
}
return window
}
}
这使视图控制器符合ASWebAuthenticationPresentationContextProviding
,并根据协议要求实现presentationAnchor(for:)
。然后,在ASWebAuthenticationSession(url:callbackURLScheme:)
的回调中加入以下内容:
// 1
guard
error == nil,
let callbackURL = callbackURL
else {
return
}
// 2
let queryItems =
URLComponents(string: callbackURL.absoluteString)?.queryItems
// 3
let token = queryItems?.first { $0.name == "token" }?.value
// 4
Auth().token = token
// 5
DispatchQueue.main.async {
let appDelegate =
UIApplication.shared.delegate as? AppDelegate
appDelegate?.window?.rootViewController =
UIStoryboard(name: "Main", bundle: Bundle.main)
.instantiateInitialViewController()
}
以下是发生的情况:
- 确保没有错误,并且设置了一个回
调
URL。 - 从回调
URL
中获取查询项目。 - 从
URL
中提取令牌。这是你之前设置的重定向中提供的令牌。 - 在
Auth
实例上设置令牌。 - 替换根视图控制器,完成登录过程。
最后,在ASWebAuthenticationSession(url:callbackURLScheme:)
下面添加以下内容:
session.presentationContextProvider = self
session.start()
这将会话的presentationContextProvider
设置为当前视图控制器。这允许iOS知道从哪里启动浏览器。然后,它启动会话,开始登录流程。
构建并运行应用程序,如有必要,在用户标签中注销。你会看到新的Sign in with Google
按钮:
点击该按钮,你会得到一个提示,允许该应用程序访问TIL
网站进行登录:
点击Continue
,该应用程序会将你重定向到谷歌,让你登录或选择一个账户来使用。完成登录过程并选择一个账户,应用程序就会将你登录进去。
接下来去哪?¶
在本章中,你学到了如何使用Imperial
和OAuth
将Google
登录整合到你的网站中。这使得用户可以用他们现有的谷歌账户登录!
下一章将向你展示如何整合另一个流行的OAuth
提供商:GitHub
。