认证

使用 Google 登录


Supabase Auth 支持 Web 平台的 Google 登录、原生应用程序(AndroidmacOS 和 iOS)以及 Chrome 扩展程序

您可以通过两种方式使用 Google 登录

先决条件#

要开始使用 Google 登录,您需要进行一些设置

设置所需的范围#

Supabase Auth 需要几个范围,授予访问用户个人资料数据的权限,您必须在 数据访问 (Scopes) 屏幕中配置这些范围

  • openid (手动添加)
  • .../auth/userinfo.email (默认添加)
  • .../auth/userinfo.profile (默认添加)

如果您添加了更多范围,尤其是那些在敏感或受限列表中的范围,您的应用程序可能需要进行验证,这可能需要很长时间。

Google 的同意屏幕会在用户登录时显示。您可以选择配置以下其中一项,以改善屏幕的外观,从而提高用户对您的信任度

  1. 通过在 Google Auth Platform 控制台的 品牌 部分进行配置,验证您的应用程序的品牌(徽标和名称)。品牌验证不是自动的,可能需要几个工作日。
  2. 设置 项目的自定义域名,以便向用户呈现与他们点击“使用 Google 登录”的网站的明确关系。
    • 一个好的方法是使用 auth.example.comapi.example.com,如果您的应用程序托管在 example.com 上。
    • 如果您没有进行此设置,用户将看到 <project-id>.supabase.co,这不会建立信任,并且可能使您的应用程序更容易受到成功的网络钓鱼攻击。

项目设置#

要支持使用 Google 登录,您需要配置 Supabase 项目的 Google 提供程序。

无论您使用应用程序代码还是 Google 的预构建解决方案来实现登录流程,您都需要在 Google Auth Platform 控制台的 客户端 部分获取客户端 ID 和客户端密钥,并配置您的项目

  1. 创建一个新的 OAuth 客户端 ID 并选择 Web 应用程序 作为应用程序类型。
  2. 授权的 JavaScript 来源 下,添加您的应用程序的 URL。这些也应配置为您的项目中的 站点 URL 或重定向配置
    • 如果您的应用程序托管在 https://example.com/app,请添加 https://example.com
    • 在本地开发时添加 https://:<port>。请记住,当您的应用程序 投入生产 时删除此项。
  3. 授权的重定向 URI 下,添加您的 Supabase 项目的回调 URL。
  4. 点击 创建 并确保保存客户端 ID 和客户端密钥。

本地开发#

要在本地开发中使用 Google 提供程序

  1. 添加一个新的环境变量
    1
    SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET="<client-secret>"
  2. supabase/config.toml 中配置提供程序
    1
    [auth.external.google]
    2
    enabled = true
    3
    client_id = "<client-id>"
    4
    secret = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET)"
    5
    skip_nonce_check = false

如果您有多个客户端 ID,例如 Web、iOS 和 Android 各一个,请用逗号连接所有客户端 ID,但请确保 Web 的客户端 ID 位于列表的第一个位置。

使用管理 API#

使用 PATCH /v1/projects/{ref}/config/auth 管理 API 端点 以编程方式配置项目的 Auth 设置。要配置 Google 提供程序,请发送以下选项

1
{
2
"external_google_enabled": true,
3
"external_google_client_id": "your-google-client-id",
4
"external_google_secret": "your-google-client-secret"
5
}

登录用户#

应用程序代码#

要使用您自己的应用程序代码进行登录按钮,请调用 signInWithOAuth 方法(或您语言的等效方法)。

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

对于隐式流程,您只需要执行这些操作。用户将被带到 Google 的同意屏幕,最后重定向到您的应用程序,并带有代表其会话的访问和刷新令牌对。

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

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

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

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

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

app/auth/callback/route.ts
1
import { NextResponse } from 'next/server'
2
// The client you created from the Server-Side Auth instructions
3
import { createClient } from '@/utils/supabase/server'
4
5
export async function GET(request: Request) {
6
const { searchParams, origin } = new URL(request.url)
7
const code = searchParams.get('code')
8
// if "next" is in param, use it as the redirect URL
9
let next = searchParams.get('next') ?? '/'
10
if (!next.startsWith('/')) {
11
// if "next" is not a relative URL, use the default
12
next = '/'
13
}
14
15
if (code) {
16
const supabase = await createClient()
17
const { error } = await supabase.auth.exchangeCodeForSession(code)
18
if (!error) {
19
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
20
const isLocalEnv = process.env.NODE_ENV === 'development'
21
if (isLocalEnv) {
22
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
23
return NextResponse.redirect(`${origin}${next}`)
24
} else if (forwardedHost) {
25
return NextResponse.redirect(`https://${forwardedHost}${next}`)
26
} else {
27
return NextResponse.redirect(`${origin}${next}`)
28
}
29
}
30
}
31
32
// return the user to an error page with instructions
33
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
34
}

在代码交换成功后,用户的会话将保存到 cookie 中。

保存 Google 令牌#

您的应用程序保存的令牌是 Supabase Auth 令牌。您的应用程序可能还需要 Google OAuth 2.0 令牌才能代表用户访问 Google 服务。

在首次登录时,您可以从会话中提取 provider_token,并将其存储在安全的存储介质中。会话可在 signInWithOAuth(隐式流程)和 exchangeCodeForSession(PKCE 流程)返回的数据中获得。

Google 默认情况下不会发送刷新令牌,因此您需要将类似以下的参数传递给 signInWithOAuth(),才能提取 provider_refresh_token

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

Google 预构建 #

大多数 Web 应用程序和网站可以使用 Google 的 个性化登录按钮一键登录自动登录,以获得最佳用户体验。

  1. 通过包含第三方脚本在您的应用程序中加载 Google 客户端库

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

  3. 选择 切换到 JavaScript 回调 选项,并输入您的回调函数名称。此函数将在登录完成后接收 CredentialResponse

    为了使您的应用程序与 Chrome 的第三方 cookie 淘汰计划兼容,请确保将 data-use_fedcm_for_prompt 设置为 true

    您的最终 HTML 代码可能如下所示

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

    1
    async function handleSignInWithGoogle(response) {
    2
    const { data, error } = await supabase.auth.signInWithIdToken({
    3
    provider: 'google',
    4
    token: response.credential,
    5
    })
    6
    }
  5. (可选) 配置一个 nonce。建议使用 nonce 以获得额外的安全性,但它是可选的。nonce 应该每次都随机生成,并且必须在 HTML 代码的 data-nonce 属性和回调函数的选项中提供。

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

    请注意,nonce 在这两个地方都应该是相同的,但由于 Supabase Auth 期望提供程序对其进行哈希处理(SHA-256,十六进制表示),因此您需要向 Google 提供哈希版本,并向 signInWithIdToken 提供未哈希版本。

    您可以使用内置的 crypto 库来获取这两个版本

    1
    // Adapted from https://mdn.org.cn/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
    2
    3
    const = (.(....(new (32))))
    4
    const = new ()
    5
    const = .()
    6
    ..('SHA-256', ).(() => {
    7
    const = .(new ())
    8
    const = .(() => .(16).(2, '0')).('')
    9
    })
    10
    11
    // Use 'hashedNonce' when making the authentication request to Google
    12
    // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method

Next.js 的一键登录#

如果您正在将 Google One-Tap 与您的 Next.js 应用程序集成,可以参考下面的示例开始使用

1
'use client'
2
3
import Script from 'next/script'
4
import { createClient } from '@/utils/supabase/client'
5
import type { accounts, CredentialResponse } from 'google-one-tap'
6
import { useRouter } from 'next/navigation'
7
8
declare const google: { accounts: accounts }
9
10
// generate nonce to use for google id token sign-in
11
const generateNonce = async (): Promise<string[]> => {
12
const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))
13
const encoder = new TextEncoder()
14
const encodedNonce = encoder.encode(nonce)
15
const hashBuffer = await crypto.subtle.digest('SHA-256', encodedNonce)
16
const hashArray = Array.from(new Uint8Array(hashBuffer))
17
const hashedNonce = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
18
19
return [nonce, hashedNonce]
20
}
21
22
const OneTapComponent = () => {
23
const supabase = createClient()
24
const router = useRouter()
25
26
const initializeGoogleOneTap = async () => {
27
console.log('Initializing Google One Tap')
28
const [nonce, hashedNonce] = await generateNonce()
29
console.log('Nonce: ', nonce, hashedNonce)
30
31
// check if there's already an existing session before initializing the one-tap UI
32
const { data, error } = await supabase.auth.getSession()
33
if (error) {
34
console.error('Error getting session', error)
35
}
36
if (data.session) {
37
router.push('/')
38
return
39
}
40
41
/* global google */
42
google.accounts.id.initialize({
43
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
44
callback: async (response: CredentialResponse) => {
45
try {
46
// send id token returned in response.credential to supabase
47
const { data, error } = await supabase.auth.signInWithIdToken({
48
provider: 'google',
49
token: response.credential,
50
nonce,
51
})
52
53
if (error) throw error
54
console.log('Session data: ', data)
55
console.log('Successfully logged in with Google One Tap')
56
57
// redirect to protected page
58
router.push('/')
59
} catch (error) {
60
console.error('Error logging in with Google One Tap', error)
61
}
62
},
63
nonce: hashedNonce,
64
// with chrome's removal of third-party cookies, we need to use FedCM instead (https://developers.google.com/identity/gsi/web/guides/fedcm-migration)
65
use_fedcm_for_prompt: true,
66
})
67
google.accounts.id.prompt() // Display the One Tap UI
68
}
69
70
return <Script onReady={initializeGoogleOneTap} src="https://#/gsi/client" />
71
}
72
73
export default OneTapComponent