Skip to content
Auth

用谷歌登录

Supabase Auth 支持网页版用 Google 登录、原生应用(安卓macOS 和 iOS)以及Chrome 扩展

🌐 Supabase Auth supports Sign in with Google for the web, native applications (Android, macOS and iOS), and Chrome extensions.

你可以用谷歌账户登录有两种方式:

🌐 You can use Sign in with Google in two ways:

先决条件 #

🌐 Prerequisites

你需要做一些设置才能开始使用 Google 登录:

🌐 You need to do some setup to get started with Sign in with Google:

  • 准备一个 Google Cloud 项目。去 Google Cloud Platform 并在必要时创建一个新项目。
  • 使用 Google 身份验证平台控制台 来注册并设置你的应用的:
    • 通过配置允许哪些 Google 用户登录你的应用,受众
    • 数据访问(范围) 定义了你的应用可以如何使用用户的 Google 数据和 API,例如访问个人资料信息等。
    • 品牌验证 会在授权界面显示徽标和名称,而不是 Supabase 项目 ID,从而提高用户留存率。品牌验证可能需要几天工作日。

设置所需权限 #

🌐 Setup required scopes

Supabase Auth 需要一些权限范围来访问你终端用户的个人资料数据,你需要在 数据访问(Scopes) 页面中进行配置:

🌐 Supabase Auth needs a few scopes granting access to profile data of your end users, which you have to configure in the Data Access (Scopes) screen:

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

如果你添加更多权限,特别是那些敏感或受限制的权限,你的应用可能需要进行审核,这可能会花很长时间。

🌐 If you add more scopes, especially those on the sensitive or restricted list your application might be subject to verification which may take a long time.

🌐 Setup consent screen branding

当用户登录时,会显示 Google 的同意屏幕。你可以选择配置以下其中一项来改善屏幕的外观,从而提升用户对你的信任感:

🌐 Google's consent screen is shown to users when they sign in. Optionally configure one of the following to improve the appearance of the screen, increasing the perception of trust by your users:

  1. 通过在 Google Auth 平台控制台的 品牌 部分配置,验证你应用的品牌(徽标和名称)。品牌验证不是自动的,可能需要几个工作日。
  2. 为你的项目设置自定义域名,让用户在点击“用 Google 登录”时能清楚地看到与网站的关联。
    • 一个不错的方法是使用 auth.example.comapi.example.com,如果你的应用托管在 example.com 上的话。
    • 如果你不设置这个,用户会看到 <project-id>.supabase.co,这让人不太放心,还可能让你的应用更容易受到成功的钓鱼攻击。

项目设置 #

🌐 Project setup

要支持使用 Google 登录,你需要为你的 Supabase 项目配置 Google 提供者。

🌐 To support Sign In with Google, you need to configure the Google provider for your Supabase project.

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

🌐 Regardless of whether you use application code or Google's pre-built solutions to implement the sign in flow, you need to configure your project by obtaining a Client ID and Client Secret in the Clients section of the Google Auth Platform console:

  1. 创建一个新的 OAuth 客户端 ID,并为应用类型选择 Web 应用
  2. Authorized JavaScript origins 下添加你的应用的 URL。这些也应该在你的项目中配置为 站点 URL 或重定向配置
    • 如果你的应用托管在 https://example.com/app 上,添加 https://example.com
    • 在本地开发时添加 http://localhost:<port>。记得在你的应用上线时将其移除。
  3. Authorized redirect URIs 下添加你的 Supabase 项目的回调 URL。
    • 从仪表板上的 Google 提供商页面 访问它。
    • 在本地开发时,使用 http://127.0.0.1:54321/auth/v1/callback
  4. 点击 Create 并确保你保存了客户端 ID 和客户端密钥。

本地开发 #

🌐 Local development

在本地开发中使用 Google 提供者:

🌐 To use the Google provider in local development:

  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。

🌐 If you have multiple client IDs, such as one for Web, iOS and Android, concatenate all of the client IDs with a comma but make sure the web's client ID is first in the list.

使用管理 API #

🌐 Using the management API

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

🌐 Use the PATCH /v1/projects/{ref}/config/auth Management API endpoint to configure the project's Auth settings programmatically. For configuring the Google provider send these options:

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

正在登录用户 #

🌐 Signing users in

应用代码 #

🌐 Application code

要在登录按钮上使用你自己的应用代码,请调用 signInWithOAuth 方法(或者你所用语言的等效方法)。

🌐 To use your own application code for the signin button, call the signInWithOAuth method (or the equivalent for your language).

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
4
5
// ---cut---
6
supabase.auth.signInWithOAuth({
7
provider: 'google',
8
})

对于隐式流,这就是你需要做的全部。用户会被带到谷歌的同意界面,最后会被重定向回你的应用,并带上表示他们会话的访问令牌和刷新令牌。

🌐 For an implicit flow, that's all you need to do. The user will be taken to Google's consent screen, and finally redirected to your app with an access and refresh token pair representing their session.

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

🌐 For a PKCE flow, for example in Server-Side Auth, you need an extra step to handle the code exchange. When calling signInWithOAuth, provide a redirectTo URL which points to a callback route. This redirect URL should be added to your redirect allow list.

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

🌐 In the browser, signInWithOAuth automatically redirects to the OAuth provider's authentication endpoint, which then redirects to your endpoint.

1
import { createClient, type Provider } from '@supabase/supabase-js';
2
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
3
const provider = 'provider' as Provider
4
5
// ---cut---
6
await supabase.auth.signInWithOAuth({
7
provider,
8
options: {
9
redirectTo: `http://example.com/auth/callback`,
10
},
11
})

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

🌐 At the callback endpoint, handle the code exchange to save the user session.

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

🌐 Create a new file at app/auth/callback/route.ts and populate with the following:

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

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

🌐 After a successful code exchange, the user's session will be saved to cookies.

保存谷歌令牌 #

🌐 Saving Google tokens

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

🌐 The tokens saved by your application are the Supabase Auth tokens. Your app might additionally need the Google OAuth 2.0 tokens to access Google services on the user's behalf.

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

🌐 On initial login, you can extract the provider_token from the session and store it in a secure storage medium. The session is available in the returned data from signInWithOAuth (implicit flow) and exchangeCodeForSession (PKCE flow).

谷歌默认不会发送刷新令牌,所以你需要像这样向 signInWithOAuth() 传递参数,才能获取 provider_refresh_token

🌐 Google does not send out a refresh token by default, so you will need to pass parameters like these to signInWithOAuth() in order to extract the provider_refresh_token:

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
4
5
// ---cut---
6
const { data, error } = await supabase.auth.signInWithOAuth({
7
provider: 'google',
8
options: {
9
queryParams: {
10
access_type: 'offline',
11
prompt: 'consent',
12
},
13
},
14
})

谷歌预建 #

🌐 Google pre-built [#google-pre-built]

大多数网页应用和网站可以使用谷歌的个性化登录按钮一键登录自动登录来获得最佳用户体验。

🌐 Most web apps and websites can use Google's personalized sign-in buttons, One Tap or automatic sign-in for the best user experience.

  1. 在你的应用中通过引入第三方脚本来加载 Google 客户端库:

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

  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。这个函数需要在全局作用域中可用,以便谷歌的代码可以找到它。

    1
    async function handleSignInWithGoogle(response) {
    2
    const { data, error } = await supabase.auth.signInWithIdToken({
    3
    provider: 'google',
    4
    token: response.credential,
    5
    })
    6
    }
  5. (可选) 配置一个随机数。建议使用随机数以增加安全性,但这是可选的。每次都应该随机生成这个随机数,并且必须同时在 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://web.nodejs.cn/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
    2
    3
    const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))
    4
    const encoder = new TextEncoder()
    5
    const encodedNonce = encoder.encode(nonce)
    6
    crypto.subtle.digest('SHA-256', encodedNonce).then((hashBuffer) => {
    7
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    8
    const hashedNonce = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
    9
    })
    10
    11
    // Use 'hashedNonce' when making the authentication request to Google
    12
    // Use 'nonce' when invoking the supabase.auth.signInWithIdToken() method

一键使用 Next.js #

🌐 One-tap with Next.js

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

🌐 If you're integrating Google One-Tap with your Next.js application, you can refer to the example below to get started:

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