crush-level-web/docs/GoogleOAuth-GIS.md

9.1 KiB
Raw Permalink Blame History

Google Identity Services (GIS) 登录集成文档

概述

使用最新的 Google Identity Services (GIS) SDK 实现 Google 登录,无需页面跳转,通过弹窗方式完成授权,用户体验更好。

新旧方式对比

旧方式OAuth 2.0 重定向流程)

用户点击按钮 → 跳转到 Google 授权页面 → 授权后重定向回应用

需要页面跳转
需要配置回调路由
用户体验不连贯

新方式Google Identity Services

用户点击按钮 → 弹出 Google 授权窗口 → 授权后直接回调

无需页面跳转
无需回调路由
用户体验流畅
更安全(弹窗隔离)

实现架构

工作流程

用户点击 "Continue with Google"
    ↓
加载 Google Identity Services SDK
    ↓
初始化 Code Client
    ↓
弹出 Google 授权窗口
    ↓
用户授权
    ↓
回调函数接收授权码
    ↓
调用后端登录接口
    ↓
登录成功,跳转到首页

核心文件

1. Google OAuth 配置 (src/lib/oauth/google.ts)

主要功能:

  • 定义 Google Identity Services 的 TypeScript 类型
  • 提供 SDK 加载方法
  • 提供 Code Client 初始化方法

关键代码:

export const googleOAuth = {
  // 加载 Google Identity Services SDK
  loadScript: (): Promise<void> => {
    return new Promise((resolve, reject) => {
      if (window.google?.accounts) {
        resolve()
        return
      }

      const script = document.createElement('script')
      script.src = 'https://accounts.google.com/gsi/client'
      script.async = true
      script.defer = true
      script.onload = () => resolve()
      script.onerror = () => reject(new Error('Failed to load SDK'))

      document.head.appendChild(script)
    })
  },

  // 初始化 Code Client获取授权码
  initCodeClient: (callback, errorCallback) => {
    return window.google.accounts.oauth2.initCodeClient({
      client_id: GOOGLE_CLIENT_ID,
      scope: GOOGLE_SCOPES,
      ux_mode: 'popup',
      callback,
      error_callback: errorCallback,
    })
  },
}

2. GoogleButton 组件 (src/app/(auth)/login/components/GoogleButton.tsx)

主要功能:

  • 加载 Google Identity Services SDK
  • 初始化 Code Client
  • 处理授权码回调
  • 调用后端登录接口

关键实现:

SDK 加载

useEffect(() => {
  const loadGoogleSDK = async () => {
    try {
      await googleOAuth.loadScript()
      console.log('Google Identity Services SDK loaded')
    } catch (error) {
      console.error('Failed to load Google SDK:', error)
      toast.error('Failed to load Google login')
    }
  }

  loadGoogleSDK()
}, [])

授权码处理

const handleGoogleResponse = async (response: GoogleCodeResponse) => {
  const deviceId = tokenManager.getDeviceId()
  const loginData = {
    appClient: AppClient.Web,
    deviceCode: deviceId,
    thirdToken: response.code, // Google 授权码
    thirdType: ThirdType.Google,
  }

  login.mutate(loginData, {
    onSuccess: () => {
      toast.success('Login successful')
      router.push('/')
    },
    onError: (error) => {
      toast.error('Login failed')
    },
  })
}

登录按钮点击

const handleGoogleLogin = async () => {
  // 确保 SDK 已加载
  if (!window.google?.accounts?.oauth2) {
    await googleOAuth.loadScript()
  }

  // 初始化 Code Client
  if (!codeClientRef.current) {
    codeClientRef.current = googleOAuth.initCodeClient(handleGoogleResponse, handleGoogleError)
  }

  // 请求授权码(弹出授权窗口)
  codeClientRef.current.requestCode()
}

环境变量配置

只需要配置客户端 ID不需要配置回调 URL

# .env.local
NEXT_PUBLIC_GOOGLE_CLIENT_ID=你的Google客户端ID

获取 Google OAuth 凭据

  1. 访问 Google Cloud Console
  2. 创建或选择项目
  3. 启用 Google+ API
  4. 创建 OAuth 2.0 客户端 ID
  5. 应用类型选择 "Web 应用"
  6. 授权的 JavaScript 来源添加:
    http://localhost:3000
    https://test.crushlevel.ai
    
  7. 授权的重定向 URI 可以留空GIS 不需要)
  8. 复制客户端 ID

后端接口要求

与之前相同,后端接收授权码并完成登录:

POST /api/auth/login

{
  "appClient": "WEB",
  "deviceCode": "设备ID",
  "thirdToken": "Google授权码",
  "thirdType": "GOOGLE"
}

后端需要:

  1. 使用授权码向 Google 交换 access_token
  2. 使用 access_token 获取用户信息
  3. 创建或更新用户
  4. 返回应用的登录 token

优势

1. 更好的用户体验

  • 无需离开当前页面
  • 弹窗授权,快速完成
  • 不打断用户操作流程

2. 更简单的实现

  • 不需要回调路由
  • 不需要处理 URL 参数
  • 不需要 state 验证
  • 代码更简洁

3. 更安全

  • 弹窗隔离,防止钓鱼
  • SDK 自动处理安全验证
  • 支持 CORS 和 CSP

4. 更现代

  • Google 官方推荐方式
  • 持续维护和更新
  • 更好的浏览器兼容性

与旧实现的对比

特性 旧方式(重定向) 新方式GIS
页面跳转 需要 不需要
回调路由 需要 不需要
State 验证 需要手动实现 SDK 自动处理
URL 参数处理 需要 不需要
用户体验
代码复杂度
维护成本

常见问题

Q: SDK 加载失败怎么办?

A:

  • 检查网络连接
  • 确认没有被广告拦截器阻止
  • 检查浏览器控制台错误信息

Q: 弹窗被浏览器拦截?

A:

  • 确保在用户点击事件中调用 requestCode()
  • 不要在异步操作后调用
  • 检查浏览器弹窗设置

Q: 授权后没有回调?

A:

  • 检查回调函数是否正确绑定
  • 查看浏览器控制台是否有错误
  • 确认 Client ID 配置正确

Q: 用户取消授权如何处理?

A:

const handleGoogleError = (error: any) => {
  // 用户取消授权不显示错误提示
  if (error.type === 'popup_closed') {
    return
  }

  toast.error('Google login failed')
}

测试清单

本地测试

  • SDK 正常加载
  • 点击按钮弹出授权窗口
  • 授权后正确回调
  • 授权码正确传递给后端
  • 登录成功后正确跳转
  • 用户取消授权的处理
  • 错误情况的处理

生产环境测试

  • 配置正确的 JavaScript 来源
  • HTTPS 证书有效
  • 环境变量配置正确
  • 后端接口正常工作
  • 不同浏览器测试

浏览器兼容性

Google Identity Services 支持:

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

安全注意事项

1. 客户端 ID 保护

虽然客户端 ID 是公开的,但仍需注意:

  • 限制授权的 JavaScript 来源
  • 定期检查使用情况
  • 发现异常及时更换

2. 授权码处理

  • 授权码只能使用一次
  • 及时传递给后端
  • 不要在客户端存储

3. HTTPS 要求

  • 生产环境必须使用 HTTPS
  • 本地开发可以使用 HTTP

迁移指南

如果你之前使用的是旧的重定向方式,迁移步骤:

  1. 更新配置文件

    • 使用新的 src/lib/oauth/google.ts
  2. 更新组件

    • 使用新的 GoogleButton.tsx
  3. 删除回调路由

    • 删除 src/app/api/auth/google/callback/route.ts
  4. 更新 Google Cloud Console

    • 添加授权的 JavaScript 来源
    • 可以移除重定向 URI可选
  5. 测试

    • 完整测试登录流程
    • 确认所有功能正常

扩展功能

1. One Tap 登录

可以添加 Google One Tap 功能,自动显示登录提示:

window.google.accounts.id.initialize({
  client_id: GOOGLE_CLIENT_ID,
  callback: handleCredentialResponse,
})

window.google.accounts.id.prompt()

2. 自动登录

可以实现自动登录功能:

window.google.accounts.id.initialize({
  client_id: GOOGLE_CLIENT_ID,
  callback: handleCredentialResponse,
  auto_select: true,
})

3. 自定义按钮样式

可以使用 Google 提供的标准按钮:

window.google.accounts.id.renderButton(document.getElementById('buttonDiv'), {
  theme: 'outline',
  size: 'large',
})

相关文档

总结

使用 Google Identity Services 是 Google 官方推荐的最新方式,相比传统的 OAuth 重定向流程:

用户体验更好 - 无需页面跳转
实现更简单 - 代码量更少
维护更容易 - 无需处理复杂的回调
更加安全 - SDK 自动处理安全验证

强烈建议新项目直接使用这种方式!