认证

使用Google登录


Supabase Auth 支持通过 Google 登录网页应用、原生 Android 应用和 Chrome 扩展程序。

先决条件

配置

要支持 Google 登录功能,您需要为 Supabase 项目配置 Google 提供商。

对于网页应用,您可以通过两种方式设置登录按钮:

应用程序代码配置

要使用您自己的应用程序代码:

  1. 在 Google Cloud 控制台中,转到同意屏幕配置页面。同意屏幕是用户同意登录您的应用时显示的界面。

  2. 授权域名下,添加您的 Supabase 项目域名,格式为 <PROJECT_ID>.supabase.co

  3. 配置以下非敏感权限范围:

    • .../auth/userinfo.email
    • ...auth/userinfo.profile
    • openid
  4. 转到API 凭证页面

  5. 点击创建凭证并选择OAuth 客户端 ID

  6. 对于应用程序类型,选择Web 应用程序

  7. 授权的 JavaScript 来源下,添加您的网站 URL。

  8. 授权的重定向 URL中,输入来自 Supabase 仪表盘 的回调 URL。展开 Google 认证提供程序部分即可显示该 URL。

  9. 完成凭证配置后,系统会显示您的客户端 ID 和密钥。将这些信息添加到 Supabase 仪表盘的 Google 认证提供程序部分

Google 预构建配置

要使用 Google 预构建的登录按钮:

  1. 在 Google Cloud 控制台中,前往同意屏幕配置页面。同意屏幕是用户同意登录您的应用时显示的界面。
  2. 根据喜好配置该屏幕,确保添加了指向您应用隐私政策和服务条款的链接。
  3. 前往API 凭据页面
  4. 点击创建凭据并选择OAuth 客户端 ID
  5. 在应用类型中选择网络应用
  6. 授权的 JavaScript 来源授权的重定向 URI部分,添加您的网站 URL。这是登录按钮出现的网站 URL,而非您的 Supabase 项目域名。如果在本地测试,请确保在授权的 JavaScript 来源部分也设置了http://localhost。这在集成 Google One-Tap 时很重要,确保您可以在本地使用它。
  7. 完成凭据配置后,将显示您的客户端 ID。将其添加到 Supabase 仪表板的 Google 认证提供商部分客户端 ID字段中。保留 OAuth 客户端 ID 和密钥为空。使用 Google 预构建方法时不需要这些。

用户登录

应用代码

要使用自己的应用代码实现登录按钮,可以调用 signInWithOAuth 方法(或对应语言的等效方法)。

1
2
3
..({ : 'google',})

对于隐式流程,这就是您需要做的全部操作。用户将被引导至Google的授权页面,最终携带代表其会话的访问令牌和刷新令牌对重定向回您的应用。

以 PKCE 流程为例,比如在服务器端身份验证中,您需要额外的步骤来处理代码交换。调用 signInWithOAuth 时,提供一个指向回调路由的 redirectTo URL。此重定向 URL 应添加到您的重定向允许列表中。

在浏览器中,signInWithOAuth 会自动重定向到 OAuth 提供商的身份验证端点,然后再重定向到您的端点。

1
2
3
4
5
6
await ..({ , : { : `http://example.com/auth/callback`, },})

在回调端点,处理代码交换以保存用户会话。

app/auth/callback/route.ts 创建一个新文件,并填充以下内容:

app/auth/callback/route.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { NextResponse } from 'next/server'// 您根据服务器端身份验证说明创建的客户端import { createClient } from '@/utils/supabase/server'export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') // 如果参数中有 "next",则将其用作重定向 URL let next = searchParams.get('next') ?? '/' if (!next.startsWith('/')) { // 如果 "next" 不是相对 URL,则使用默认值 next = '/' } if (code) { const supabase = await createClient() const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { const forwardedHost = request.headers.get('x-forwarded-host') // 负载均衡器之前的原始来源 const isLocalEnv = process.env.NODE_ENV === 'development' if (isLocalEnv) { // 我们可以确定中间没有负载均衡器,因此无需关注 X-Forwarded-Host return NextResponse.redirect(`${origin}${next}`) } else if (forwardedHost) { return NextResponse.redirect(`https://${forwardedHost}${next}`) } else { return NextResponse.redirect(`${origin}${next}`) } } } // 将用户重定向到带有说明的错误页面 return NextResponse.redirect(`${origin}/auth/auth-code-error`)}

成功完成代码交换后,用户会话将被保存至cookies。

保存Google令牌

您的应用保存的是Supabase Auth令牌。如果应用需要代表用户访问Google服务,则可能还需要Google OAuth 2.0令牌。

在初始登录时,您可以从会话中提取 provider_token 并存储到安全介质中。该会话可通过 signInWithOAuth(隐式流程)和 exchangeCodeForSession(PKCE流程)的返回数据获取。

Google默认不发送刷新令牌,因此您需要向 signInWithOAuth() 传递如下参数才能获取 provider_refresh_token

1
2
3
4
5
6
7
8
9
const { , } = await ..({ : 'google', : { : { : 'offline', : 'consent', }, },})

Google 预构建方案

大多数Web应用和网站都可以使用Google的个性化登录按钮One Tap自动登录来获得最佳用户体验。

  1. 在应用中加载Google客户端库,引入第三方脚本:

    1
    <script src="https://accounts.google.com/gsi/client" async></script>
  2. 使用HTML代码生成器自定义Google登录按钮的外观、功能和行为。

  3. 选择_Swap to JavaScript callback_选项,并输入回调函数名称。该函数将在登录完成时接收CredentialResponse

    为确保应用兼容Chrome的第三方Cookie淘汰计划,请务必将data-use_fedcm_for_prompt设置为true

    最终HTML代码可能如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <div id="g_id_onload" data-client_id="<client ID>" data-context="signin" data-ux_mode="popup" data-callback="handleSignInWithGoogle" data-nonce="" data-auto_select="true" data-itp_support="true" data-use_fedcm_for_prompt="true"></div><div class="g_id_signin" data-type="standard" data-shape="pill" data-theme="outline" data-text="signin_with" data-size="large" data-logo_alignment="left"></div>
  4. 创建handleSignInWithGoogle函数,接收CredentialResponse并将包含的令牌传递给Supabase。该函数需在全局作用域中可用,以便Google代码能找到它。

    1
    2
    3
    4
    5
    6
    async function handleSignInWithGoogle(response) { const { data, error } = await supabase.auth.signInWithIdToken({ provider: 'google', token: response.credential, })}
  5. (可选) 配置nonce。建议使用nonce增强安全性,但这是可选的。nonce应每次随机生成,并需同时在HTML代码的data-nonce属性和回调函数选项中提供。

    1
    2
    3
    4
    5
    6
    7
    async function handleSignInWithGoogle(response) { const { data, error } = await supabase.auth.signInWithIdToken({ provider: 'google', token: response.credential, nonce: '<NONCE>', })}

    注意两处的nonce应相同,但由于Supabase Auth要求提供商对其进行哈希处理(SHA-256,十六进制表示),您需要向Google提供哈希版本,而向signInWithIdToken提供未哈希版本。

    可以使用内置的crypto库获取两个版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 改编自 https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_stringconst = (.(....(new (32))))const = new ()const = .()..('SHA-256', ).(() => { const = .(new ()) const = .(() => .(16).(2, '0')).('')})// 向Google发起认证请求时使用'hashedNonce'// 调用supabase.auth.signInWithIdToken()方法时使用'nonce'

使用 Next.js 实现一键登录

如果您正在将 Google 一键登录集成到 Next.js 应用程序中,可以参考以下示例开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
'use client'import Script from 'next/script'import { createClient } from '@/utils/supabase/client'import { CredentialResponse } from 'google-one-tap'import { useRouter } from 'next/navigation'import { useEffect } from 'react'const OneTapComponent = () => { const supabase = createClient() const router = useRouter() // 生成用于 Google ID 令牌登录的 nonce const generateNonce = async (): Promise<string[]> => { const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))) const encoder = new TextEncoder() const encodedNonce = encoder.encode(nonce) const hashBuffer = await crypto.subtle.digest('SHA-256', encodedNonce) const hashArray = Array.from(new Uint8Array(hashBuffer)) const hashedNonce = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') return [nonce, hashedNonce] } useEffect(() => { const initializeGoogleOneTap = () => { console.log('正在初始化 Google 一键登录') window.addEventListener('load', async () => { const [nonce, hashedNonce] = await generateNonce() console.log('Nonce: ', nonce, hashedNonce) // 在初始化一键登录 UI 前检查是否已有会话 const { data, error } = await supabase.auth.getSession() if (error) { console.error('获取会话时出错', error) } if (data.session) { router.push('/') return } /* global google */ google.accounts.id.initialize({ client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, callback: async (response: CredentialResponse) => { try { // 将响应中的 ID 令牌发送到 Supabase const { data, error } = await supabase.auth.signInWithIdToken({ provider: 'google', token: response.credential, nonce, }) if (error) throw error console.log('会话数据: ', data) console.log('已成功通过 Google 一键登录') // 重定向到受保护页面 router.push('/') } catch (error) { console.error('Google 一键登录时出错', error) } }, nonce: hashedNonce, // 随着 Chrome 移除第三方 Cookie,我们需要改用 FedCM (https://developers.google.com/identity/gsi/web/guides/fedcm-migration) use_fedcm_for_prompt: true, }) google.accounts.id.prompt() // 显示一键登录 UI }) } initializeGoogleOneTap() return () => window.removeEventListener('load', initializeGoogleOneTap) }, []) return ( <> <Script src="https://accounts.google.com/gsi/client" /> <div id="oneTap" className="fixed top-0 right-0 z-[100]" /> </> )}export default OneTapComponent

Google 授权界面

Google 授权界面

默认情况下,Google 授权界面会显示回调 URL 的根域名,即 Google 发送认证响应的目标地址。对于 Supabase Auth 来说,这就是您 Supabase 项目的域名 (https://<your-project-ref>.supabase.co)

如果这不是您想要的,您可以为 Supabase 项目使用自定义域名。在应用程序中创建 Supabase 客户端并初始化认证流程时,可以使用该域名作为项目域名。这样它就会显示在 Google 授权界面上。如果您希望在授权界面上显示应用名称和徽标,必须将应用提交给 Google 进行验证