👆建議你可以使用影片子母畫面功能或全螢幕播放來獲得最佳的觀賞體驗,👇下方是本篇教學的相關筆記。
前往 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=""
安裝 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>
安裝 vue3-google-login 套件
npm install -D google-auth-library
在 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>
在 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>
在 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 的夥伴。
範例程式碼
參考資料
請問為何只有 「使用 Auth Code 進行驗證」 步驟的 new OAuth2Client
會帶 clientId
、redirectUri
那些參數呢?
這是 OAuth 的標準與設計上的不同,在使用 Auth Code 進行驗證,是必須要有 clientId
、redirectUri
的資訊,才准予驗證,否則將會驗證失敗。
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