使用 Google OAuth 登录

OAuth 是一种开放的委派授权协议,它允许网站或应用程序之间授予访问权限,而无需向被授予访问权限的各方暴露用户密码。许多公司和网站都使用这种非常常见的访问委派方式,通过提供 OAuth 授权的第三方(如 GoogleFacebook)来识别用户。让我们允许我们的用户使用 Google OAuth 登录我们的应用程序。此选项需要从 Google 开发者控制台获取客户端 ID 和客户端密钥。可以通过以下步骤获取它们:

  1. Google 开发者控制台 (https://console.developers.google.com/) 中创建一个新项目。

  2. OAuth 同意屏幕标签上选择 “外部”。

  3. 在 “凭据” 标签上的 “创建凭据” 下拉选项中选择 “OAuth 客户端 ID”,然后为 “应用程序类型” 选择 “Web 应用程序”。

  4. 在 “名称” 字段中提供你的 OAuth 客户端 ID 的名称,并在 “已授权的重定向 URI” 字段中提供重定向 URI,以便 Google 在用户在 Google 同意页面上进行身份验证后将用户重定向到这些 URI

  5. 启用 Google People API,该 API 通过 “库” 标签中的 API 库提供对个人资料和联系人信息的访问。

按照前面的步骤设置开发者帐户并创建客户端 ID 和客户端密钥后,你就可以在下一节中将 Google OAuth 添加到后端身份验证了。让我们开始吧。

为后端认证添加 Google OAuth

为了让某人通过 Google 登录,我们需要将他们发送到 Google 登录页面。在那里,他们将登录自己的帐户,并被重定向回我们的应用程序,其中包含他们的 Google 登录详细信息,我们将从中提取 Google 代码并将其发送回 Google 以获取我们可以在应用程序中使用用户数据。此过程需要 googleapis Node.js 模块,这是一个用于使用 Google API 的客户端库。

让我们按照以下步骤安装并在我们的代码中采用它:

  1. 通过 npm 安装 googleapis Node.js 模块:

    $ npm i googleapis
  2. 创建一个包含你的凭据的文件,以便 Google 知道是谁发出的请求:

    // backend/src/config/google.js
    export default {
      clientId: '<客户端 ID>',
      clientSecret: '<客户端密钥>',
      redirect: 'http://localhost:3000/login'
    }

    请注意,你必须将上面的 <客户端 ID> 和 <客户端密钥> 值替换为你从 Google 开发者控制台获得的 ID 和密钥。另请注意,redirect 选项中的 URL 必须与你的 Google 应用 API 设置中 “已授权的重定向 URI” 中的 URI 匹配。

  3. 使用 Google OAuth 生成一个 Google 身份验证 URL,以便将用户发送到 Google 同意页面,从而获得用户检索访问令牌的权限,如下所示:

    // backend/src/modules/public/login/_routes/google/url.js
    import Router from 'koa-router'
    import { google } from 'googleapis'
    import googleConfig from 'config/google'
    const router = new Router()
    router.get('/google/url', async (ctx, next) => {
      const oauth = new google.auth.OAuth2(
        googleConfig.clientId,
        googleConfig.clientSecret,
        googleConfig.redirect
      )
      const scopes = [
        'https://www.googleapis.com/auth/userinfo.email',
        'https://www.googleapis.com/auth/userinfo.profile',
      ]
      const url = oauth.generateAuthUrl({
        access_type: 'offline',
        prompt: 'consent',
        scope: scopes
      })
      ctx.body = url
    })

    作用域决定了当用户登录并生成 URL 时,我们想要从用户那里获取哪些信息和权限。在我们的例子中,我们想要获取用户电子邮件和个人资料信息的权限:userinfo.emailuserinfo.profile。用户在 Google 同意页面上进行身份验证后,Google 会将用户重定向回我们的应用程序,其中包含一堆身份验证数据和一个用于访问用户数据的授权代码。

  4. 从上一步中 Google 附加在返回 URL 中的身份验证数据中提取 code 参数的值。我们将在下一节中回到 Node.js 模块,该模块可以帮助我们稍后从 URL 查询中提取 code 参数。现在,假设我们已经提取了 code 值并将其发送到服务器端,以便使用以下基本代码结构中的 Google OAuth2 实例请求令牌:

    // backend/src/modules/public/login/_routes/google/me.js
    import Router from 'koa-router'
    import { google } from 'googleapis'
    import jwt from 'jsonwebtoken'
    import pool from 'core/database/mysql'
    import config from 'config'
    import googleConfig from 'config/google'
    const router = new Router()
    router.get('/google/me', async (ctx, next) => {
      // 从 URL 查询中获取代码。
      const code = ctx.query.code
      // 创建一个新的 google oauth2 客户端实例。
      const oauth2 = new google.auth.OAuth2(
        googleConfig.clientId,
        googleConfig.clientSecret,
        googleConfig.redirect
      )
      //...
    })
  5. 使用我们刚刚提取的代码从 Google 获取令牌,并将它们传递给 Google People (google.people),使用 get 方法获取用户数据,并在 personFields 查询参数中指定需要返回的与人相关的字段:

    // backend/src/modules/public/login/_routes/google/me.js
    ...
    const {tokens} = await oauth2.getToken(code)
    oauth.setCredentials(tokens)
    const people = google.people({
      version: 'v1',
      auth: oauth2,
    })
    const me = await people.people.get({
      resourceName: 'people/me',
      personFields: 'names,emailAddresses'
    })

    你可以看到,在上面的代码中,我们只想要从 Google 获取与人相关的两个字段,即 namesemailAddresses。你可以在 https://developers.google.com/people/api/rest/v1/people/get 上找到你想从 Google 获取的其他与人相关的字段。如果访问成功,我们应该从 Google 获得 JSON 格式的用户数据,然后我们可以从该数据中提取电子邮件,以确保它将在下一步中与我们数据库中的用户匹配。

  6. 仅从 Google 个人数据中检索第一个电子邮件,并查询我们的数据库以查看是否已存在具有该电子邮件的用户:

    // backend/src/modules/public/login/_routes/google/me.js
    ...
    let email = me.data.emailAddresses[0].value
    let users = []
    try {
      users = await pool.query('SELECT * FROM users WHERE email = ?',
        [email])
    } catch(err) {
      ctx.throw(400, err.sqlMessage)
    }
  7. 如果不存在具有该电子邮件的用户,则向客户端发送 “signup required” 消息以及来自 Google 的用户数据,并要求用户在我们的应用程序中注册一个帐户:

    // backend/src/modules/public/login/_routes/google/me.js
    ...
    if (users.length === 0) {
      ctx.body = {
        user: me.data,
        message: 'signup required'
      }
      return
    }
    let user = users[0]
  8. 如果存在匹配项,则使用载荷和 JWT 密钥签署一个 JWT,然后将令牌 (JWT) 发送到客户端:

    // backend/src/modules/public/login/_routes/google/me.js
    ...
    let payload = { name: user.name, email: user.email }
    let token = jwt.sign(payload, config.JWT_SECRET, { expiresIn: 1 *
      60 })
    ctx.body = {
      user: payload,
      message: 'logged in ok',
      token: token
    }

就这样。在前面的几个步骤中,你已经成功地在服务器端添加了 Google OAuth。接下来,我们将在下一节中了解如何在客户端使用 Nuxt 完成 Google OAuth 的身份验证。让我们开始吧。

有关 googleapis Node.js 模块的更多信息,请访问 https://github.com/googleapis/google-api-nodejs-client。

为 Google OAuth 创建前端认证

Google 将用户重定向回我们的应用程序时,我们将在重定向 URL 上获得一堆数据,例如:

http://localhost:3000/login?code=4%2F1QGpS37E21TcgQhhIvJZlK1cG4M1jpPJ0I_XPQ
grFjvKUFUJQ3aYuO1zYsqPmKgNb4Wfd8ito88yDjUTD6CKD3E&scope=email%20profile%20h
ttps%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https%3A%2F%2Fwww
.googleapis.com%2Fauth%2Fuserinfo.profile%20openid&authuser=1&prompt=consen
t

乍一看,它很难阅读和理解,但它只是一个查询字符串,其参数附加到我们的重定向 URL

<redirect URL>?
code=4/1QFvWYDSrW...
&scope=email profile...
&authuser=1
&prompt=consent

我们可以使用 Node.js 模块 query-string 来解析 URL 中的查询字符串,例如:

const queryString = require('query-string')
const parsed = queryString.parse(location.search)
console.log(parsed)

然后你将在浏览器的控制台中得到以下 JavaScript 对象:

{
  "authuser": "1",
  "code": "4/1QFvWYDSrWLklhIgRfVR0LJy6Pk0gn5TkjTKWKlRr9pdZveGAHV_pMrxBhicy7Zd6d9nfz0IQrcLl-VGS-Gu9Xk",
  "prompt": "consent",
  "scope": "email profile https://www.googleapis.com/auth/user…//www.googleapis.com/auth/userinfo.profile openid"
}

正如你在上一节中学到的,code 参数是上述重定向 URL 中我们最感兴趣的,因为我们需要将其发送到服务器端,以便通过 googleapis Node.js 模块获取 Google 用户数据。因此,让我们安装 query-string 并在我们的 Nuxt 应用程序中创建前端身份验证,步骤如下:

  1. 通过 npm 安装 query-string Node.js 模块:

    $ npm i query-string
  2. 在登录页面上创建一个按钮,并绑定一个名为 loginWithGoogle 的方法来分发 store 中的 getGoogleUrl 方法,如下所示:

    // frontend/pages/login.vue
    <button @click="loginWithGoogle">Google 登录</button>
    export default {
      methods: {
        async loginWithGoogle() {
          try {
            await this.$store.dispatch('getGoogleUrl')
          } catch (error) {
            let errorData = error.response.data
            this.formError = errorData.message
          }
        }
      }
    }
  3. getGoogleUrl 方法中调用 API 中的 /api/public/login/google/url 路由,如下所示:

    // frontend/store/actions.js
    export default {
      async getGoogleUrl(context) {
        const { data } = await this.$axios.$get('/api/public/login/google/url')
        window.location.replace(data)
      }
    }

    /api/public/login/google/url 路由将返回一个 Google URL,然后我们可以使用它将用户重定向到 Google 登录页面。在那里,如果用户有多个 Google 帐户,他们将决定登录哪个帐户。

  4. Google 将用户重定向回登录页面时,从返回的 URL 中提取查询部分,并将其发送到 store 中的 loginWithGoogle 方法,如下所示:

    // frontend/pages/login.vue
    export default {
      async mounted () {
        let query = window.location.search
        if (query) {
          try {
            await this.$store.dispatch('loginWithGoogle', query)
          } catch (error) {
            // 处理错误
          }
        }
      }
    }
  5. 使用 query-string 从上述查询部分的 code 参数中提取代码,并使用 $axios 将其发送到我们的 API /api/public/login/google/me,如下所示:

    // frontend/store/actions.js
    import queryString from 'query-string'
    export default {
      async loginWithGoogle (context, query) {
        const parsed = queryString.parse(query)
        const { data } = await this.$axios.$get('/api/public/login/google/me', {
          params: {
            code: parsed.code
          }
        })
        if (data.message === 'signup required') {
          localStorage.setItem('user', JSON.stringify(data.user))
          this.$router.push({ name: 'signup'})
        } else {
          cookies.set('auth', data)
          context.commit('setAuth', data)
        }
      }
    }

    当从服务器收到 “signup required” 消息时,我们将用户重定向到注册页面。但是,如果收到带有 JWT 的消息,那么我们可以设置一个 cookie 并将身份验证数据提交到 storestate 中。我们将注册页面留给你的想象力和努力,因为它是一个收集用户数据以存储在数据库中的表单。

  6. 最后,使用 npm run dev 运行 Nuxt 应用程序。你应该在浏览器上的 localhost:3000 看到应用程序正在运行。你可以使用 Google 登录,然后访问受 JWT 保护的受限页面——就像本地身份验证一样。

就这样——这些是你使用 Google OAuth API 登录用户的基本步骤。一点也不难,对吧?我们还可以使用 Nuxt Auth 模块来实现几乎与我们在这里完成的相同的功能。使用此模块,你可以使用 Auth0FacebookGitHubLaravel PassportGoogle 登录用户。如果你正在为 Nuxt 寻找快速、简单且零样板的身份验证支持,它可能是你项目的一个不错的选择。有关此 Nuxt 模块的更多信息,请访问 https://auth.nuxtjs.org/ 。现在,让我们在下一节中总结一下你在本章中学到的内容。

你可以在我们的 GitHub 仓库的 /chapter-12/nuxt-universal/cross-domain/jwt/axios-module/ 中找到上述带有 Google OAuth 的登录选项。

有关 query-string Node.js 模块用法的更多信息,请访问 https://www.npmjs.com/package/query-string。