iT邦幫忙

2023 iThome 鐵人賽

DAY 19
1
影片教學

Nuxt 3 快速入門系列 第 19

[影片教學] Nuxt 3 串接 Google OAuth 登入

  • 分享至 

  • xImage
  •  

Yes

👆建議你可以使用影片子母畫面功能或全螢幕播放來獲得最佳的觀賞體驗,👇下方是本篇教學的相關筆記。


建立 Google OAuth 用戶 ID

前往 Google Cloud Console 建立 Google OAuth 用戶 ID,並保存好用戶端編號用戶端密鑰

開啟專案下 nuxt.config.ts 檔案,添加 Runtime Config

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      googleClientId: '這邊放上你的 Google Client ID'
    },
    googleClientSecret: '這邊放上你的 Google Client Secret'
  }
})

在專案目錄下建立 .env 檔案,用來覆蓋所建立的 Runtime Config:

NUXT_PUBLIC_GOOGLE_CLIENT_ID=""
NUXT_GOOGLE_CLIENT_SECRET=""

建立 Google Login 按鈕

安裝 vue3-google-login 套件

npm install -D vue3-google-login

plugins 目錄下建立一個檔案 ./plugins/vue3-google-login.clitn.js

import vue3GoogleLogin from 'vue3-google-login'

export default defineNuxtPlugin((nuxtApp) => {
  const runtimeConfig = useRuntimeConfig()
  const { googleClientId: GOOGLE_CLIENT_ID } = runtimeConfig.public

  nuxtApp.vueApp.use(vue3GoogleLogin, {
    clientId: GOOGLE_CLIENT_ID
  })
})

pages 目錄下建立一個檔案 ./pages/index.vue

<template>
  <div class="flex flex-col items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
    <div class="flex w-full max-w-md flex-col items-center justify-center">
      <h1 class="my-8 flex text-center text-3xl font-bold tracking-tight text-emerald-500">
        Nuxt App
      </h1>
      <ClientOnly>
        <GoogleLogin :callback="callback" popup-type="TOKEN">
          <button
            class="flex rounded-md border border-gray-100 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
          >
            <span class="text-slate-500 group-hover:text-slate-600">使用 Google 進行登入</span>
          </button>
        </GoogleLogin>
      </ClientOnly>
    </div>
  </div>
</template>

<script setup>
const callback = (response) => {
  console.log(response)
}
</script>

修改 app.vue 檔案:

<template>
  <div>
    <NuxtPage />
  </div>
</template>

建立後端 API 驗證使用者

安裝 vue3-google-login 套件

npm install -D google-auth-library

使用 Credential 進行驗證

server/api/auth 目錄下建立一個檔案 ./server/api/auth/google-credential.js

import { OAuth2Client } from 'google-auth-library'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const oauth2Client = new OAuth2Client()

  const ticket = await oauth2Client.verifyIdToken({
    idToken: body.credential
  })

  const payload = ticket.getPayload()

  if (!payload) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid token'
    })
  }

  return {
    id: payload.sub,
    name: payload.name,
    avatar: payload.picture,
    email: payload.email,
    emailVerified: payload.email_verified
  }
})

pages/login 目錄下建立一個檔案 ./pages/login/googleCredential.vue

<template>
  <div class="flex flex-col items-center justify-center px-4 py-6">
    <div class="flex w-full max-w-md flex-col items-center justify-center">
      <h1 class="my-8 flex text-center text-3xl font-bold tracking-tight text-emerald-500">
        Credential
      </h1>

      <ClientOnly>
        <GoogleLogin :callback="callback" prompt />
      </ClientOnly>

      <div class="mt-4 text-gray-700">
        {{ userInfo }}
      </div>
    </div>
  </div>
</template>

<script setup>
const userInfo = ref(null)

const callback = async (response) => {
  if (!response?.credential) {
    // 登入失敗
    return
  }

  const { data } = await useFetch('/api/auth/google-credential', {
    method: 'POST',
    body: {
      credential: response.credential
    }
  })

  userInfo.value = data.value
}
</script>

使用 Access Token 進行驗證

server/api/auth 目錄下建立一個檔案 ./server/api/auth/google-auth-token.js

import { OAuth2Client } from 'google-auth-library'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const oauth2Client = new OAuth2Client()
  oauth2Client.setCredentials({ access_token: body.accessToken })

  const userInfo = await oauth2Client
    .request({
      url: 'https://www.googleapis.com/oauth2/v3/userinfo'
    })
    .then((response) => response.data)
    .catch(() => null)

  if (!userInfo) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid token'
    })
  }

  return {
    id: userInfo.sub,
    name: userInfo.name,
    avatar: userInfo.picture,
    email: userInfo.email,
    emailVerified: userInfo.email_verified
  }
})

pages/login 目錄下建立一個檔案 ./pages/login/googleAuthToken.vue

<template>
  <div class="flex flex-col items-center justify-center px-4 py-6">
    <div class="flex w-full max-w-md flex-col items-center justify-center">
      <h1 class="my-8 flex text-center text-3xl font-bold tracking-tight text-emerald-500">
        Auth Token
      </h1>

      <button
        class="flex rounded-md border border-gray-100 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
        @click="handleGoogleLogin"
      >
        <span class="text-slate-500 group-hover:text-slate-600">使用 Google 進行登入</span>
      </button>

      <div class="mt-4 text-gray-700">
        {{ userInfo }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { googleTokenLogin } from 'vue3-google-login'

const runtimeConfig = useRuntimeConfig()
const { googleClientId: GOOGLE_CLIENT_ID } = runtimeConfig.public

const userInfo = ref(null)

const handleGoogleLogin = async () => {
  const accessToken = await googleTokenLogin({
    clientId: GOOGLE_CLIENT_ID
  }).then((response) => response?.access_token)

  if (!accessToken) {
    // 登入失敗
    return
  }

  const { data } = await useFetch('/api/auth/google-auth-token', {
    method: 'POST',
    body: {
      accessToken
    }
  })

  userInfo.value = data.value
}
</script>

使用 Auth Code 進行驗證

server/api/auth 目錄下建立一個檔案 ./server/api/auth/google-auth-code.js

import { OAuth2Client } from 'google-auth-library'

const runtimeConfig = useRuntimeConfig()
const { googleClientId: GOOGLE_CLIENT_ID } = runtimeConfig.public
const { googleClientSecret: GOOGLE_CLIENT_SECRET } = runtimeConfig

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const oauth2Client = new OAuth2Client({
    clientId: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    redirectUri: 'http://localhost:3000'
  })

  const { tokens } = await oauth2Client.getToken(body.authCode)

  oauth2Client.setCredentials({ access_token: tokens.access_token })

  const userInfo = await oauth2Client
    .request({
      url: 'https://www.googleapis.com/oauth2/v3/userinfo'
    })
    .then((response) => response.data)
    .catch(() => null)

  await oauth2Client.revokeCredentials()

  if (!userInfo) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid token'
    })
  }

  return {
    id: userInfo.sub,
    name: userInfo.name,
    avatar: userInfo.picture,
    email: userInfo.email,
    emailVerified: userInfo.email_verified
  }
})

pages/login/googleAuthCode 目錄下建立一個檔案 ./pages/login/googleAuthCode.vue

<template>
  <div class="flex flex-col items-center justify-center px-4 py-6">
    <div class="flex w-full max-w-md flex-col items-center justify-center">
      <h1 class="my-8 flex text-center text-3xl font-bold tracking-tight text-emerald-500">
        Auth Code
      </h1>

      <button
        class="flex rounded-md border border-gray-100 bg-white px-4 py-2 text-sm font-medium shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
        @click="handleGoogleLogin"
      >
        <span class="text-slate-500 group-hover:text-slate-600">使用 Google 進行登入</span>
      </button>

      <div class="mt-4 text-gray-700">
        {{ userInfo }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { googleAuthCodeLogin } from 'vue3-google-login'

const runtimeConfig = useRuntimeConfig()
const { googleClientId: GOOGLE_CLIENT_ID } = runtimeConfig.public

const userInfo = ref(null)

const handleGoogleLogin = async () => {
  const authCode = await googleAuthCodeLogin({
    clientId: GOOGLE_CLIENT_ID
  }).then((response) => response?.code)

  if (!authCode) {
    // 登入失敗
    return
  }

  const { data } = await useFetch('/api/auth/google-auth-code', {
    method: 'POST',
    body: {
      authCode
    }
  })

  userInfo.value = data.value
}
</script>

感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。

範例程式碼

參考資料


上一篇
[影片教學] Nuxt 3 Runtime Config & App Config
下一篇
[影片教學] Nuxt 3 Cookie 的設置與使用
系列文
Nuxt 3 快速入門30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Dylan
iT邦新手 1 級 ‧ 2023-11-05 21:59:49

請問為何只有 「使用 Auth Code 進行驗證」 步驟的 new OAuth2Client 會帶 clientIdredirectUri 那些參數呢?

Ryan iT邦新手 1 級 ‧ 2023-11-06 10:57:57 檢舉

這是 OAuth 的標準與設計上的不同,在使用 Auth Code 進行驗證,是必須要有 clientIdredirectUri 的資訊,才准予驗證,否則將會驗證失敗。

Auth Code 在 OAuth 的流程中,只是用來做臨時驗證使用的授權碼,主要用來更安全的取得 Access Token,而 Access Token 則是實際用來取得使用者授權資料的憑證令牌。

Auth Code 通常是短效期、一次性的。
Access Token 已經是核發出來,且比較長時間的憑證。

這裡有一些參考資料
https://www.oauth.com/oauth2-servers/access-tokens/authorization-code-request/
https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest#oauth2

我要留言

立即登入留言