跳转至

第23章:GitHub认证

在上一章中,你学到了如何用谷歌来认证用户。在本章中,你将看到如何在此基础上,让用户用他们的GitHub账户来登录。

GitHub设置你的应用程序

为了能够在你的应用程序中使用GitHub OAuth,你必须首先在GitHub上注册该应用程序。在浏览器中,转到 https://github.com/settings/developers。点击Register a new application

img

Note

你必须有一个GitHub账户才能完成本章。如果你没有,请访问 https://github.com/join 来创建一个。本章还假定你在前一章中把Imperial作为依赖项添加到你的项目中。

在表格中填写一个合适的名字,例如:Vapor TIL。为这个应用程序设置主页URLhttp://localhost:8080,并提供一个合理的描述。将授权回调URL设置为http://localhost:8080/oauth/github。一旦用户允许你的应用程序访问他们的数据,GitHub就会重定向到这个URL

img

点击Register application。创建应用程序后,该网站将带你回到应用程序的信息页面。该页面提供了客户ID。点击Generate a new client secret以获得客户秘密:

img

Note

你必须保持这些东西的安全和可靠。你的秘密允许你访问GitHubAPI,你不应该分享或检查秘密到源代码控制。你应该像对待密码一样对待它。

Imperial集成

现在你已经在GitHub上注册了你的应用程序,你可以开始整合Imperial。首先,在Xcode中打开Package.swift并替换:

.product(name: "ImperialGoogle", package: "Imperial")

为以下内容:

.product(name: "ImperialGoogle", package: "Imperial"),
.product(name: "ImperialGitHub", package: "Imperial")

这就把ImperialGitHub库作为一个依赖项添加进去。接下来,打开ImperialController.swift,在import Fluent下面添加以下内:

import ImperialGitHub

这样你的代码就可以看到ImperialGitHub功能。接下来,在processGoogleLogin(request:token:)下添加以下内容:

func processGitHubLogin(request: Request, token: String)
  throws -> EventLoopFuture<ResponseEncodable> {
    return request.eventLoop.future(request.redirect(to: "/"))
  }

这定义了一个处理GitHub登录的方法,类似于Google登录的初始处理程序。这个处理方法只是将用户重定向到主页。一旦处理完GitHub的重定向,Imperial会使用这个方法作为最后的回调。

接下来,通过在boot(routes:)的底部添加以下内容来设置Imperial路由:

guard let githubCallbackURL =
  Environment.get("GITHUB_CALLBACK_URL") else {
    fatalError("GitHub callback URL not set")
}
try routes.oAuth(
  from: GitHub.self,
  authenticate: "login-github",
  callback: githubCallbackURL,
  completion: processGitHubLogin)

下面是这个的作用:

  • 从环境变量中获取回调URL - 这是你在GitHub注册应用程序时设置的URL
  • ImperialGitHub OAuth路由器与你的应用程序的路由注册。
  • 告诉Imperial使用GitHub处理程序。
  • /login-github请求设置为触发OAuth流程的路由。这是应用程序用来允许用户通过GitHub登录的路由。
  • 提供回调URLImperial
  • 将完成处理程序设置为processGitHubLogin(request:token:) - 你在上面创建的方法。

和以前一样,你需要用环境变量向Imperial提供GitHub给你的客户ID和客户秘密。你还必须提供重定向的URL。在文本编辑器中打开.env,在文件的底部添加以下内容:

GITHUB_CALLBACK_URL=http://localhost:8080/oauth/github
GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID>
GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET>

添加之前由GitHub生成的客户端ID和密码。

Note

请确保你仍然为GOOGLE_CALLBACK_URLGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET设置了环境变量,否则你的应用程序将无法启动。

与网络认证整合

和上一章一样,与常规登录的体验相匹配是很重要的。同样,当用户第一次使用GitHub登录时,你会创建一个新用户。你可以用用户的OAuth令牌来使用GitHubAPI

ImperialController.swift的底部,添加一个新类型来解码来自GitHubAPI的数据:

struct GitHubUserInfo: Content {
  let name: String
  let login: String
}

GitHubAPI的请求会返回许多字段。然而,你只关心登录,也就是用户名,以及名字。

接下来,在GitHubUserInfo下,添加以下内容:

extension GitHub {
  // 1
  static func getUser(on request: Request)
    throws -> EventLoopFuture<GitHubUserInfo> {
      // 2
      var headers = HTTPHeaders()
      try headers.add(
        name: .authorization, 
        value: "token \(request.accessToken())")
      headers.add(name: .userAgent, value: "vapor")

      // 3
      let githubUserAPIURL: URI = "https://api.github.com/user"
      // 4
      return request
        .client
        .get(githubUserAPIURL, headers: headers)
        .flatMapThrowing { response in
          // 5
          guard response.status == .ok else {
            // 6
            if response.status == .unauthorized {
              throw Abort.redirect(to: "/login-github")
            } else {
              throw Abort(.internalServerError)
            }
          }
          // 7
          return try response.content
            .decode(GitHubUserInfo.self)
      }
  }
}

下面是这个的作用:

  1. ImperialGitHub服务添加一个新方法,从GitHubAPI中获取用户的详细信息。
  2. 通过在授权头中添加OAuth令牌来设置请求的头信息。注意,GitHub没有使用标准的承载授权头,所以你必须手动定义该头。还要设置user-agent头,因为GitHubAPI需要这个。
  3. 设置请求的URL--这是GitHubAPI,用于获取用户的信息。这使用了VaporURI,而Client需要这个。
  4. 使用request.client发送一个HTTP请求。get()向提供的URL发送一个HTTP GET请求。解除返回的未来响应。
  5. 确保响应状态是200 OK
  6. 否则,如果响应是401 Unauthorized,则返回到登录页面,或者返回一个错误。
  7. GitHub响应中的数据进行解码并返回结果。

接下来,将processGitHubLogin(request:token:)的主体替换为以下内容:

// 1
return try GitHub
  .getUser(on: request)
  .flatMap { userInfo in
    // 2
    return User
      .query(on: request.db)
      .filter(\.$username == userInfo.login)
      .first()
      .flatMap { foundUser in
        guard let existingUser = foundUser else {
          // 3
          let user = User(
            name: userInfo.name,
            username: userInfo.login,
            password: UUID().uuidString)
          // 4
          return user
            .save(on: request.db)
            .flatMap {
              // 5
              request.session.authenticate(user)
              return generateRedirect(on: request, for: user)
          }
        }
        // 6
        request.session.authenticate(existingUser)
        return generateRedirect(on: request, for: existingUser)
    }
}

以下是新代码的作用:

  1. GitHub获取用户信息。
  2. 通过查找用户名的登录属性,查看该用户是否存在于数据库中。
  3. 如果用户不存在,用GitHub上的用户信息中的名字和用户名创建一个新的User。将密码设置为UUID,因为你不需要它。
  4. 保存用户并解开返回的未来。
  5. Request上调用session.authenticate(_:)以在会话中保存创建的用户,这样网站就允许访问。使用前一章的generateRedirect(on:for:)来重定向到主页。
  6. 如果用户已经存在,在会话中验证用户并重定向到主页。同样,使用generateRedirect(on:for:)来创建重定向。

最后要做的是在网站上添加一个按钮,让用户使用新的功能! 打开login.leaf,在</form>下,添加以下内容:

<a href="/login-github">
  <img class="mt-3" src="/images/sign-in-with-github.png"
   alt="Sign In With GitHub">
</a>

本章的示例项目包含一个新的图片,sign-in-with-github.png,用于显示Sign in with GitHub按钮。这将图像添加为/login-github的链接--提供给Imperial开始登录的路线。构建并运行该应用程序,然后在浏览器中访问http://localhost:8080。点击Create An Acronym,应用程序就会带你到登录页面。你会看到新的Sign in with GitHub按钮在Sign in with Google按钮旁边:

img

点击新按钮,应用程序会带你到一个GitHub页面,允许TIL应用程序访问你的信息:

img

点击你在那里看到的Authorize按钮,应用程序会将你重定向到主页。进入All Users屏幕,你会看到你的新用户账户。如果你创建了一个缩写,应用程序也会使用这个新用户。

iOS整合

就像用谷歌登录一样,你也应该提供在iOS上用GitHub登录的功能。在上一章中,你已经完成了大部分的工作 :] 。

iOSGoogleLogin(_:)下面,创建一个新的路由处理程序,用于在iOS上用GitHub登录:

func iOSGitHubLogin(_ req: Request) -> Response {
  // 1
  req.session.data["oauth_login"] = "iOS"
  // 2
  return req.redirect(to: "/login-github")
}

这个新的路由处理程序做两件事:

  1. 在请求的会话中设置一个标志,将其标记为iOS登录的尝试。
  2. 重定向到/login-github以触发与GitHubOAuth流程。

boot(routes:)的底部注册新路由:

routes.get("iOS", "login-github", use: iOSGitHubLogin)

这就把对/iOS/login-githubGET请求路由到iosGitHubLogin(_:)。这就是你在TILApp中需要做的所有事情,以支持iOSGitHub的登录! 构建并运行该项目,并打开本章的iOS启动项目。

本章的iOS启动项目在GitHub的登录页面上包含一个新的按钮。在LoginTableViewController.swift中有一个相应的方法,在用户点击新按钮时调用。在signInWithGithubButtonTapped(_:)中加入以下内容:

// 1
guard let githubAuthURL =
  URL(string: "http://localhost:8080/iOS/login-github") 
else {
  return
}
// 2
let scheme = "tilapp"
// 3
let session = ASWebAuthenticationSession(
  url: githubAuthURL, 
  callbackURLScheme: scheme) { callbackURL, error in
  // 4
  guard 
    error == nil, 
    let callbackURL = callbackURL 
  else { 
    return 
  }

  let queryItems = URLComponents(
    string: callbackURL.absoluteString
  )?.queryItems
  let token = queryItems?.first { $0.name == "token" }?.value
  // 5
  Auth().token = token
  // 6
  DispatchQueue.main.async {
    let appDelegate = 
      UIApplication.shared.delegate as? AppDelegate
    appDelegate?.window?.rootViewController =
      UIStoryboard(
        name: "Main", 
        bundle: Bundle.main).instantiateInitialViewController()
  }
}
// 7
session.presentationContextProvider = self
session.start()

以下是新代码的作用:

  1. 创建一个用于登录GitHubURL。这是你之前在TILApp中创建的URL
  2. 将方案设置为tilapp--这是你重定向到的方案。更多信息见第22章,"谷歌认证"。
  3. 使用该方案和URL创建一个`ASWebAuthenticationSession'的实例。
  4. 确保有一个回调的URL并且没有错误。从回调URL中提取令牌。
  5. 使用Auth在钥匙串中设置令牌。
  6. 通过改变根视图控制器到主导航视图控制器来完成用户的登录。
  7. presentationContextProvider设置为LoginViewController并开始会话。

构建并运行应用程序,如有必要,在Users标签中注销。在登录页面,你会看到新的Sign in with GitHub按钮:

img

点击新按钮,应用程序会要求你确认你想使用TILApp来登录:

img

点击Continue。如果你已经在模拟器上登录了GitHub,该应用会直接登录。否则,你会看到GitHubOAuth界面,允许访问:

img

登录GitHub,如果你已经批准了TILApp,应用程序就会将你登入。否则,GitHub会要求你确认对TIL应用程序的访问,就像网站一样。一旦确认,应用程序就会登录。

接下来去哪?

在本章中,你学到了如何使用ImperialOAuthGitHub登录整合到你的网站中。这是对谷歌和第一方登录体验的补充,让你的用户可以选择一系列的认证方式。

在下一章中,你将学习如何实现Apple登录,让你的用户有第三个选择,使用外部认证服务来注册你的应用程序。