Vue3 接入 Google 登录:极简教程

公司目前做的是一款激光雕刻产品,主要用于出口海外,需要开发一个web社区网站用于桌面端模型生成后发布到社区进行分享和交流。说到出海产品,google第三方登录是必须接入的,这两天和后端一起开发完成了此功能,现将流程大概梳理如下。

首先当然是看Google的OAuth 2.0文档,了解其流程和参数。

文档地址:developers.google.com/identity/pr...

第一步:在Google API Console创建 OAuth 2.0 凭据

第二步:接入方式和流程

我们可以看到文档提供了多种应用类型的接入方式,对于web来说主要是红框里的两种。

1、适用于服务器端 Web 应用

我们目前采用的方式,需要后端存储google用户信息。

接入流程:前端唤起 Google 授权 → 前端获取授权码 → 后端用授权码换 token → 验证用户信息并返回自有 token

2、适用于 JavaScript Web 应用

主要前端完成google登录,后端只需要接收前端返回的token进行验证即可。

接入流程:用户点击登录 → 授权 -> 前端直接获得id_token + 用户信息 -> id_token 发给后端验证 → 完成登录。

另外对于前端交互来说均有两种可供选择:

1、popup模式:在当前页面弹窗授权,体验更友好。

2、redirect模式:跳转新页面授权后重定向回来。

popup模式,采用vue3-google-login第三方依赖。需要注意的点:

1、前端测试通过code换取access_token时,postman需要设置请求头"Content-Type: application/x-www-form-urlencoded",否则会报错"invalid_grant"。

2、Google凭据那里不需要配置重定向URI,且后端用code换取access_token所传的参数redirect_uri应该为"postmessage",否则会报错"redirect_uri_mismatch"。

3、code不能重复使用。

以下是直接可用的前端代码(popup模式):

GoogleLoginBtn.vue

xml 复制代码
<template>  <GoogleLogin    :client-id="googleClientId"    popup-type="CODE"    :callback="handleGoogleSuccess"    :error="handleGoogleError"  >    <img class="google-login-icon" src="@/assets/icons/google.png" alt="google-login">    <button v-if="false" class="google-login-button" type="button">      <span class="google-mark">G</span>      <span class="google-label">{{ buttonLabel }}</span>    </button>  </GoogleLogin></template><script setup lang="ts">import { ElMessage } from 'element-plus'import { GoogleLogin, type CallbackTypes } from 'vue3-google-login'import { useUserStore } from '@/stores/user'interface Props {  buttonLabel?: string}interface Emits {  (e: 'success'): void  (e: 'error'): void}withDefaults(defineProps<Props>(), {  buttonLabel: 'Google 账号快捷登录',})const emit = defineEmits<Emits>()const userStore = useUserStore()const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefinedconst handleGoogleSuccess = async (response: CallbackTypes.CodePopupResponse) => {  if (!response?.code) {    ElMessage.error('未获取到 Google 授权码')    emit('error')    return  }  const success = await userStore.userLoginByGoogleCode(response.code)  if (success) {    emit('success')    return  }  emit('error')}const handleGoogleError = (_error: unknown) => {  ElMessage.error('Google 授权失败,请重试')  emit('error')}</script><style scoped lang="scss">.google-login-button {  width: 176px;  height: 44px;  border: 1px solid #d9d9d9;  border-radius: 10px;  background: #fff;  display: flex;  align-items: center;  justify-content: center;  gap: 8px;  cursor: pointer;  transition: all 0.2s ease;}.google-login-button:hover {  border-color: #c7c7c7;  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);  transform: translateY(-1px);}.google-mark {  font-size: 18px;  font-weight: 700;  color: #ea4335;}.google-label {  font-size: 13px;  color: #333;  white-space: nowrap;}.google-login-icon {  width: 48px;  height: 48px;  cursor: pointer;}</style>

redirect模式,期间由于踩了上面提的popup模式的坑,也改过一版redirect模式,没有采用第三方依赖。

以下是直接可用的前端代码(redirect模式):

GoogleLoginBtn.vue

xml 复制代码
<template>  <button class="google-login-trigger" type="button" @click="handleGoogleLogin">    <img class="google-login-icon" src="@/assets/icons/google.png" alt="google-login" />    <span class="sr-only">{{ buttonLabel }}</span>  </button></template><script setup lang="ts">import { ElMessage } from 'element-plus'interface Props {  buttonLabel?: string}interface Emits {  (e: 'success'): void  (e: 'error'): void}withDefaults(defineProps<Props>(), {  buttonLabel: 'Google 账号快捷登录',})const emit = defineEmits<Emits>()const GOOGLE_STATE_KEY = 'google_oauth_state'const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefinedconst googleRedirectUri = (import.meta.env.VITE_GOOGLE_REDIRECT_URI as string | undefined)  || `${window.location.origin}/front/community/home`const createOAuthState = () => {  if (window.crypto?.randomUUID) {    return window.crypto.randomUUID()  }  return `${Date.now()}_${Math.random().toString(36).slice(2)}`}const handleGoogleLogin = () => {  if (!googleClientId) {    ElMessage.error('未配置 Google Client ID,无法登录')    emit('error')    return  }  const state = createOAuthState()  sessionStorage.setItem(GOOGLE_STATE_KEY, state)  const query = new URLSearchParams({    client_id: googleClientId,    redirect_uri: googleRedirectUri,    response_type: 'code',    scope: 'openid profile email',    state,  })  window.location.assign(`https://accounts.google.com/o/oauth2/v2/auth?${query.toString()}`)}</script><style scoped lang="scss">.google-login-trigger {  display: inline-flex;  align-items: center;  justify-content: center;  border: none;  background: transparent;  padding: 0;  cursor: pointer;}.google-login-icon {  width: 48px;  height: 48px;  cursor: pointer;}.sr-only {  position: absolute;  width: 1px;  height: 1px;  padding: 0;  margin: -1px;  overflow: hidden;  clip: rect(0, 0, 0, 0);  white-space: nowrap;  border: 0;}</style>

App.vue

xml 复制代码
<template>  <Layout :show-top-bar="showTopBar">    <router-view />  </Layout></template><script setup lang="ts">import Layout from './components/Layout.vue'import { useRoute, useRouter } from 'vue-router'import { computed, onMounted, nextTick, ref, watch } from 'vue'import { useI18n } from 'vue-i18n'import { ElMessage } from 'element-plus'import { useUserStore } from '@/stores/user'const route = useRoute()const router = useRouter()const userStore = useUserStore()const { t } = useI18n()const isProcessingGoogleOAuth = ref(false)const GOOGLE_STATE_KEY = 'google_oauth_state'const showTopBar = computed(() => {  const from = route.query.from as string  localStorage.setItem('from', from || 'community')  return from !== 'pc-home'})const getSingleQueryValue = (value: unknown) => {  if (Array.isArray(value)) {    return value[0] || ''  }  return typeof value === 'string' ? value : ''}const clearGoogleOAuthQuery = async () => {  const nextQuery = { ...route.query }  delete nextQuery.code  delete nextQuery.scope  delete nextQuery.authuser  delete nextQuery.prompt  delete nextQuery.state  delete nextQuery.error  delete nextQuery.error_description  await router.replace({    path: route.path,    query: nextQuery,  })}const processGoogleOAuthCallback = async () => {  const code = getSingleQueryValue(route.query.code)  const oauthError = getSingleQueryValue(route.query.error)  const incomingState = getSingleQueryValue(route.query.state)  if ((!code && !oauthError) || isProcessingGoogleOAuth.value) {    return  }  isProcessingGoogleOAuth.value = true  try {    if (oauthError) {      ElMessage.error(`Google OAuth failed: ${oauthError}`)      return    }    const expectedState = sessionStorage.getItem(GOOGLE_STATE_KEY)    sessionStorage.removeItem(GOOGLE_STATE_KEY)    if (expectedState && expectedState !== incomingState) {      ElMessage.error('Google OAuth state validation failed')      return    }    const success = await userStore.userLoginByGoogleCode(code)    if (success) {      ElMessage.success(t('auth.loginSuccess'))    }  } finally {    await clearGoogleOAuthQuery()    isProcessingGoogleOAuth.value = false  }}watch(  () => route.fullPath,  () => {    void processGoogleOAuthCallback()  },  { immediate: true })onMounted(async () => {  await nextTick()  const from = route.query.from as string  console.log('route.query.from:', from)})</script><style scoped>* {  margin: 0;  padding: 0;  box-sizing: border-box;}body {  font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;  line-height: 1.5;  color: #333;  background-color: #f5f5f5;}.content-placeholder {  text-align: center;  padding: 60px 20px;  color: #666;}.content-placeholder h1 {  font-size: 36px;  margin-bottom: 20px;  color: #333;}</style>
相关推荐
weixin199701080162 小时前
货铺头商品详情页前端性能优化实战
java·前端·python
new code Boy2 小时前
NestJS、Nuxt.js 和 Next.js
前端·后端
进击切图仔3 小时前
执行 shell 脚本 5 种方式对比
前端·chrome
局i3 小时前
React 简单地图组件封装:基于高德地图 API 的实践(附源码)
前端·javascript·react.js
执行部之龙3 小时前
AI对话平台核心技术解析
前端
yuki_uix3 小时前
防抖(Debounce):从用户体验到手写实现
前端·javascript
HelloReader3 小时前
Flutter 进阶 UI搭建 iOS 风格通讯录应用(十一)
前端
wjj不想说话3 小时前
在 Vue 2.6 微前端架构中,我们为什么放弃了 Vuex 管理页面状态?
vue.js
张元清3 小时前
每个 React 开发者都需要的 10 个浏览器 API Hooks
前端·javascript·面试