iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0
Modern Web

Nuxt 3 學習筆記系列 第 19

[Day 19] Nuxt 3 串接 Google OAuth 登入

  • 分享至 

  • xImage
  •  

前言

跟隨著本系列學習 Nuxt 3 的夥伴們,應該對 Nuxt 3 有一點熟悉了,接下來會分享如何建立一個簡易的部落格網站,再結合 Nuxt 3 提供可以針對搜尋引擎最佳化的配置函數來為部落格加強 SEO。這篇要介紹網站的會員系統常會使用到第三方登入,將以 Google OAuth 為例來實際於 Nuxt 3 中做串接。

串接 Google OAuth 登入

首先,我們需要有一組 Google OAuth 使用的 Client ID,你可以到 Google Console 新增一個「OAuth 2.0 用戶端 ID」,這裡我就不再贅述網頁應用程式用的申請過程。

這邊小提醒一下,在建立 OAuth Client ID 時,已授權的 JavaScript 來源,記得填寫上您的正式環境或開發環境的 Domain,且建議使用 HTTPS。
https://ithelp.ithome.com.tw/upload/images/20221004/20152617t4ruz8Ihwc.png

完成後,記得保管好用戶端密碼用戶端 ID (Client ID) 是我們稍後會需要的,用戶端編號格式大概如:

168152363730-b37gnijdpa2rdvvbq0qc29cjh4082t3b.apps.googleusercontent.com

我們將這組 Client ID,放置在 Nuxt 的 Runtime Config 之中。調整 ./nuxt.config.ts 內容,在 runtimeConfig.public 添加 googleClientId

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

接下來,安裝 Vue 的 Google OAuth 插件,這邊使用的是 vue3-google-login,也有詳細的說明文件可以參考。

使用 NPM 安裝 vue3-google-login。

npm install -D vue3-google-login

建立 Nuxt 3 插件來使用 vue3-google-login,新增 ./plugins/vue3-goolge-login.client.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
  })
})

接著我們在元件中可以直接使用 <GoogleLogin> 元件,並添加一個 callback 屬性;此外,我使用了 Nuxt 3 提供的 <ClientOnly> 元件,將 <GoogleLogin> 包裹起來,以確保該元件僅在客戶端做渲染,以免登入按鈕在初始化發生問題。

<template>
  <div>
    <ClientOnly>
      <GoogleLogin :callback="callback" />
    </ClientOnly>
  </div>
</template>

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

接著我們啟動 Nuxt 伺服器,這邊我會習慣使用 npm run dev -- --https 來啟用 HTTPS 做測試,就能發現使用 Google 帳號登入成功後,所返回的 Credential。
https://i.imgur.com/aThIP5k.gif

One Tap prompt

你可以在 <GoogleLogin> 元件添加 prompt 屬性並設為 true,這樣就能同時啟用 Google 一鍵登入 (One Tap prompt) 的功能囉!

<GoogleLogin :callback="callback" prompt />

或者也可以在 onMounted 中呼叫 vue3-google-logingoogleOneTap() 方法,來單獨使用 One Tap prompt。

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

onMounted(() => {
  googleOneTap()
    .then((response) => {
      console.log(response)
    })
    .catch((error) => {
      console.error(error)
    })
})

</script>

自訂按鈕

如果你想自訂登入按鈕的樣式,可以在 <GoogleLogin> 的預設插槽 (Slot) 做建立。

<template>
  <GoogleLogin :callback="callback">
    <button>使用 Google 進行登入</button>
  </GoogleLogin>
</template>

使用自訂按鈕會讓 OAuth 流程稍微有點不一樣,當你登入成功後預設會回傳 Auth Code

如果設定屬性 popup-type="TOKEN",則回傳 Access Token

<template>
  <GoogleLogin :callback="callback" popup-type="TOKEN">
    <button>使用 Google 進行登入</button>
  </GoogleLogin>
</template>

使用 googleTokenLogin()

在元件中我們也可以自己建立 handleGoogleLogin 點擊事件,呼叫 googleTokenLogin() 方法並傳入設定在 Runtime Config 中的 Google Client ID,這樣點擊登入按鈕就能處理 Google 登入取得 Access Token

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

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


const handleGoogleLogin = () => {
  googleTokenLogin({
    clientId: GOOGLE_CLIENT_ID
  }).then((response) => {
    console.log(response)
  })
}
</script>

建立一個登入按鈕來呼叫 handleGoogleLogin 點擊事件。

<template>
  <div>
    <button
      type="button"
      @click="handleGoogleLogin"
    >
      使用 Google 繼續
    </button>
</template>

使用 vue3-google-login 提供的 googleTokenLogin() 方法,我們就能取得 Google 使用者的 Access Token 囉!
https://i.imgur.com/fTDZP9A.gif

使用 googleAuthCodeLogin()

我們也可以使用 googleAuthCodeLogin() 來取得 Auth Code

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

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


const handleGoogleLogin = () => {
  googleAuthCodeLogin({
    clientId: GOOGLE_CLIENT_ID
  }).then((response) => {
    console.log(response)
  })
}
</script>

https://ithelp.ithome.com.tw/upload/images/20221004/20152617YVpDI898fN.png

伺服器端驗證

當使用者於前端成功登入後,通常會傳至後端進行登入或記錄使用者,再產生使用於網站的 Token、Cookie 或 Session 等,以供後續的網站驗證做使用。

我們可使用 google-auth-library 於後端進行一系列的驗證或取得使用者資訊。

使用 NPM 安裝 google-auth-library

npm install -D google-auth-library

接下來,我們就能依照不同的登入方式取得的 CredentialAccess TokenAuth Code 送至後端做驗證。

驗證 Access Token

新增一個 Server API,只接受 POST 方法,在 Body 中夾帶 accessToken 發送至後端。

建立 ./server/api/auth/google.post.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)

  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,
  }
})

調整元件內的登入流程。

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

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

const userInfo = ref()

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', {
    method: 'POST',
    body: {
      accessToken
    },
    initialCache: false
  })

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

當我們使用 Google OAuth 登入成功後,會取得 Access Token,並將其傳至 Server API,/api/auth/google 接收 Access Token 並使用 Google API 取得使用者的資訊,最後回傳給前端。
https://i.imgur.com/yeK4duW.gif

驗證 Credential

在元件中,我們使用的登入方式如果是 Google 渲染的預設按鈕One Tap prompt,回傳值就會包含 Credential,我們將就可使用下面修改後的 Server API 進行驗證。

./server/api/auth/google.post.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
  }
})

驗證 Auth Code

我們使用的登入方式如果是呼叫 vue3-google-logingoogleAuthCodeLogin(),回傳值就會包含 Auth Code,我們就可使用下面修改後的 Server API 進行驗證。

./server/api/auth/google.post.js 內容修改為如下:

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

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const oauth2Client = new OAuth2Client({
    clientId: '你的 Google Client ID',
    clientSecret: '你的 Google Client Secret',
    redirectUri: '你的 Google Redirect Uri'
  })

  let { 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)

  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,
  }
})

小結

這篇文章記錄了實現了串接 Google OAuth 登入,並將 Access Token 資訊發送至後端進行驗證,大家可以在依照使用情境自己挑選登入方式及驗證方式,後續也能將使用者資訊儲存到資料庫中,有了資料庫我們就能依照使用者資訊,來比對資料庫進行註冊、驗證登入及產生後續的 Session 或 Cookie 等。


感謝大家的閱讀,這是我第一次參加 iThome 鐵人賽,請鞭小力一些,也歡迎大家給予建議 :)
如果對這個 Nuxt 3 系列感興趣,可以訂閱接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。

範例程式碼

參考資料


上一篇
[Day 18] Nuxt 3 Runtime Config & App Config
下一篇
[Day 20] Nuxt 3 Cookie 的設置與 JWT 的搭配
系列文
Nuxt 3 學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Wolke
iT邦新手 1 級 ‧ 2022-10-16 16:28:16

在一開始
npm run dev -- --https
https://localhost:3000 就一直跳 500

改成 --http
Sign in with Google 按紐有出現
但是按了 彈跳後 就沒反應了...

看更多先前的回應...收起先前的回應...
Ryan iT邦新手 2 級 ‧ 2022-10-16 22:46:08 檢舉

嗨,您好

我這邊使用乾淨的 Nuxt 3 專案,目前測試是沒有問題。

依照您的描述,看起來插件是有安裝成功,所以在 HTTP 下是有按鈕渲染出來,但 HTTPS 的情況,想詢問您 500 error 或 console 是否有顯示的錯誤訊息,可以在提供多一點資訊,我看是否能幫助您解決。

也可以站內信留下聯繫方式,協助您看一下專案的配置。

Wolke iT邦新手 1 級 ‧ 2022-10-17 10:54:40 檢舉

500
fetch failed ()

at async $fetchRaw2 (./node_modules/ohmyfetch/dist/shared/ohmyfetch.c2a48baf.mjs:132:20)
at async ./.nuxt/dev/index.mjs:457:20
at async ./.nuxt/dev/index.mjs:526:64
at async ./.nuxt/dev/index.mjs:106:22
at async ./node_modules/h3/dist/index.mjs:592:19
at async Server.nodeHandler (./node_modules/h3/dist/index.mjs:538:7)

Wolke iT邦新手 1 級 ‧ 2022-10-17 10:56:08 檢舉

server side:
[nuxt] [request error] [unhandled] [500] fetch failed ()
at async $fetchRaw2 (./node_modules/ohmyfetch/dist/shared/ohmyfetch.c2a48baf.mjs:132:20)
at async ./.nuxt/dev/index.mjs:457:20
at async ./.nuxt/dev/index.mjs:526:64
at async ./.nuxt/dev/index.mjs:106:22
at async ./node_modules/h3/dist/index.mjs:592:19
at async Server.nodeHandler (./node_modules/h3/dist/index.mjs:538:7)
[nuxt] [request error] [unhandled] [500] fetch failed ()
at async $fetchRaw2 (./node_modules/ohmyfetch/dist/shared/ohmyfetch.c2a48baf.mjs:132:20)
at async ./.nuxt/dev/index.mjs:457:20
at async ./.nuxt/dev/index.mjs:526:64
at async ./.nuxt/dev/index.mjs:106:22
at async ./node_modules/h3/dist/index.mjs:592:19
at async nodeHandler (./node_modules/h3/dist/index.mjs:538:7)
at async ufetch (./node_modules/unenv/runtime/fetch/index.mjs:9:17)
at async $fetchRaw2 (./node_modules/ohmyfetch/dist/shared/ohmyfetch.c2a48baf.mjs:132:20)
at async Object.errorhandler [as onError] (./.nuxt/dev/index.mjs:354:29)

Wolke iT邦新手 1 級 ‧ 2022-10-17 11:28:29 檢舉

找到了 
node 版本不能用 18 我切成 16 就都正常了

Wolke iT邦新手 1 級 ‧ 2022-10-17 11:29:47 檢舉

我要留言

立即登入留言