跳转至

有时用户希望登录一个网站,但不想每登录一个网站就创建一个帐号,可以使用已经创建过的第三方支持OAuth的帐号登录。以Google帐号为例。

OAuth 2.0

OAuth 2.0是一个用户身份认证框架,允许第三方应用访问用户信息。我们可以通过OAuth使用Google帐号登录我们之前创建的网站。我们用自己的Google帐号首先向Goolge发出用户身份认证请求,你可以允许我们的应用访问你的Google用户数据,例如电子邮件地址、头像、昵称等。允许后Google会发送你的应用一个Token,你的应用可以使用这个Token来访问Google的API。

在进行OAuth 2.0身份认证之前,你必须要有一个Google账号。

Imperial

按照OAuth的流程写Google的认证流程是极其繁琐的事,幸好已经有三方库完成了这些繁琐的过程,这个库就是Imperial

package.swift

Swift
...
        .package(url: "https://github.com/vapor/auth.git", from: "2.0.1"),
        .package(url: "https://github.com/vapor-community/Imperial.git", from: "0.7.0")
    ],
    targets: [
        .target(name: "App", dependencies: ["FluentPostgreSQL", "Authentication", "Vapor", "Leaf", "Imperial"]),
...

ImperialController.swift

Swift
import Vapor
import Authentication
import Imperial

struct ImperialController: RouteCollection {
    func boot(router: Router) throws {

    }
}

routes.swift

Swift
import Vapor

/// Register your application's routes here.
public func routes(_ router: Router) throws {

    ...

    let imperialController = ImperialController()
    try router.register(collection: imperialController)
}

上面我们配置了工程中支持OAuth认证的部分,之后我们需要在Google上注册我们web应用。因为Google在中国属性被墙状态,所以必须连接VPN代理才能正常访问。

在Google上注册应用

注册地址

google oauth

google oauth config screen

google oauth credential

注册好我们的应用后就可以准备使用OAuth了。

ImperialController

Swift
//
//  ImperialController.swift
//  App
//
//  Created by joker on 2018/12/16.
//

import Vapor
import Authentication
import Imperial

struct GoogleUserInfo: Content {
    let email: String
    let name: String
}

extension Google {
    static func getUser(on req: Request) throws -> Future<GoogleUserInfo> {
        var headers = HTTPHeaders()
        headers.bearerAuthorization = try BearerAuthorization(token: req.accessToken())

        let googleAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json"

        return try req.client().get(googleAPIURL, headers: headers).map(to: GoogleUserInfo.self, { res in
            guard res.http.status == .ok else {
                if res.http.status == .unauthorized {
                    throw Abort.redirect(to: "/login-google")
                } else {
                    throw Abort(.internalServerError)
                }
            }
            return try res.content.syncDecode(GoogleUserInfo.self)
        })
    }
}

struct ImperialController: RouteCollection {
    func boot(router: Router) throws {

        guard let callbackURL = Environment.get("GOOGLE_CALLBACK_URL") else {
            fatalError("Callback URL not set")
        }

        try router.oAuth(
            from: Google.self,
            authenticate: "login-google",
            callback: callbackURL,
            scope: ["profile", "email"],
            completion: processGoogleLogin)
    }

    func processGoogleLogin(_ req: Request, token: String) throws -> Future<ResponseEncodable> {
        return try Google.getUser(on: req).flatMap(to: ResponseEncodable.self, { userInfo in
            return User.query(on: req).filter(\.username == userInfo.email).first().flatMap(to: ResponseEncodable.self, { foundUser in
                guard let existingUser = foundUser else {
                    let user = User(name: userInfo.name, username: userInfo.email, password: "")
                    return user.save(on: req).map(to: ResponseEncodable.self, { user in
                        try req.authenticate(user)
                        return req.redirect(to: "/")
                    })
                }

                try req.authenticateSession(existingUser)
                return req.future(req.redirect(to: "/"))

            })
        })
    }
}

login.leaf

HTML
...
</form>
<a href="/login-google">
    <img class="mt-3" src="/images/sign-in-with-google.png" alt="Sign In With Google">
</a>
}
#embed("base")

google oauth xcode config

google oauth complete

评论