Supabase Remix认证

该子模块为在Remix应用程序中实现用户身份验证提供了方便的帮助。

安装Remix助手库#

1npm install @supabase/auth-helpers-remix

此库支持以下工具版本:

  • Remix: >=1.7.2

设置环境变量

在项目的API设置中检索项目URL和匿名密钥以设置以下环境变量。对于本地开发,您可以将其设置为.env文件。参见示例.

.env
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

加载器

加载器函数在组件呈现之前立即在服务器上运行。它们响应路由上的所有GET请求。您可以通过调用 createServerClient函数并将SUPABASE_URLSUPABASE_ANON_KEY以及请求响应传递给它,来创建经过身份验证的超级数据库客户端。

1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import { createServerClient } from '@supabase/auth-helpers-remix'
3
4export const loader = async ({ request }) => {
5  const response = new Response()
6  // an empty response is required for the auth helpers
7  // to set cookies to manage auth
8
9  const supabaseClient = createServerClient(
10    process.env.SUPABASE_URL,
11    process.env.SUPABASE_ANON_KEY,
12    { request, response }
13  )
14
15  const { data } = await supabaseClient.from('test').select('*')
16
17  // in order for the set-cookie header to be set,
18  // headers must be returned as part of the loader response
19  return json(
20    { data },
21    {
22      headers: response.headers,
23    }
24  )
25}

Supabase将设置cookie头来管理用户的认证会话,因此,response.headers必须从Loader函数返回。

Action#

动作函数在服务器上运行,并响应对路由的HTTP请求,而不是GET - POST、PUT、PATCH、DELETE等。你可以通过调用createServerClient函数并将你的SUPABASE_URLSUPABASE_ANON_KEY以及RequestResponse传递给它来创建一个经过认证的Supabase客户端。

1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import { createServerClient } from '@supabase/auth-helpers-remix'
3
4export const action = async ({ request }) => {
5  const response = new Response()
6
7  const supabaseClient = createServerClient(
8    process.env.SUPABASE_URL,
9    process.env.SUPABASE_ANON_KEY,
10    { request, response }
11  )
12
13  const { data } = await supabaseClient.from('test').select('*')
14
15  return json(
16    { data },
17    {
18      headers: response.headers,
19    }
20  )
21}

Supabase将设置cookie头来管理用户的认证会话,因此,response.headers必须从Action函数中返回。

会话和用户

你可以通过使用getSession函数检查用户的会话来确定用户是否被认证。

1const {
2  data: { session },
3} = await supabaseClient.auth.getSession()

The session contains a user property.

1const user = session?.user

这是访问登录用户的推荐方法。也有一个getUser()函数,但如果会话已经过期,它不会刷新。

客户端

为了在浏览器中使用Supabase客户端--在useEffect中获取数据或订阅实时事件--我们需要多做一些工作。Remix不包括使环境变量对浏览器可用的方法,所以我们需要从root.jsx路由中的loader函数将它们连接到window中。

app/root.jsx
1export const loader = () => {
2  const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
3  return json({
4    env: {
5      SUPABASE_URL,
6      SUPABASE_ANON_KEY,
7    },
8  })
9}

对于Node以外的环境,这些可能不会被存储在process.env中。

接下来,我们在组件中调用useLoaderData钩子来获取env对象。

app/root.jsx
1const { env } = useLoaderData()

然后,添加一个<script>标签,将这些环境变量附加到window中。这应该被放在app/root.jsx中的<Scripts />组件之前。

app/root.jsx
1<script
2  dangerouslySetInnerHTML={{
3    __html: `window.env = ${JSON.stringify(env)}`,
4  }}
5/>

Node的完整例子:

app/root.jsx
1import { json } from '@remix-run/node' // change this import to whatever runtime you are using
2import {
3  Form,
4  Links,
5  LiveReload,
6  Meta,
7  Outlet,
8  Scripts,
9  ScrollRestoration,
10  useLoaderData,
11} from '@remix-run/react'
12import { createBrowserClient, createServerClient } from '@supabase/auth-helpers-remix'
13
14export const meta = () => ({
15  charset: 'utf-8',
16  title: 'New Remix App',
17  viewport: 'width=device-width,initial-scale=1',
18})
19
20export const loader = () => {
21  const { SUPABASE_URL, SUPABASE_ANON_KEY } = process.env
22  return json({
23    env: {
24      SUPABASE_URL,
25      SUPABASE_ANON_KEY,
26    },
27  })
28}
29
30export default function App() {
31  const { env } = useLoaderData()
32
33  return (
34    <html lang="en">
35      <head>
36        <Meta />
37        <Links />
38      </head>
39      <body>
40        <Outlet />
41        <ScrollRestoration />
42        <script
43          dangerouslySetInnerHTML={{
44            __html: `window.env = ${JSON.stringify(env)}`,
45          }}
46        />
47        <Scripts />
48        <LiveReload />
49      </body>
50    </html>
51  )
52}

现在我们可以在我们的组件中调用createBrowserClient来获取客户端的数据,或者订阅实时事件--数据库中的变化。

身份认证

现在,认证是基于cookie的,用户可以通过操作在服务器端签入和签出。

给出这个Remix <Form />组件。

1<Form method="post">
2  <input type="text" name="email" />
3  <input type="password" name="password" />
4  <button type="submit">Go!</button>
5</Form>

注册

任何来自supabase-js的支持的认证策略都可以在服务器端工作。这就是你如何处理简单的 emailpassword认证。

1export const action = async ({ request }) => {
2  const { email, password } = Object.fromEntries(await request.formData())
3  const response = new Response()
4
5  const supabaseClient = createServerClient(
6    process.env.SUPABASE_URL,
7    process.env.SUPABASE_ANON_KEY,
8    { request, response }
9  )
10
11  const { data, error } = await supabaseClient.auth.signUp({
12    email,
13    password,
14  })
15
16  // in order for the set-cookie header to be set,
17  // headers must be returned as part of the loader response
18  return json(
19    { data, error },
20    {
21      headers: response.headers,
22    }
23  )
24}

登录

任何来自supabase-js的支持的认证策略都可以在服务器端工作。这就是你如何处理简单的 emailpassword认证。

1export const action = async ({ request }) => {
2  const { email, password } = Object.fromEntries(await request.formData())
3  const response = new Response()
4
5  const supabaseClient = createServerClient(
6    process.env.SUPABASE_URL,
7    process.env.SUPABASE_ANON_KEY,
8    { request, response }
9  )
10
11  const { data, error } = await supabaseClient.auth.signInWithPassword({
12    email: String(loginEmail),
13    password: String(loginPassword),
14  })
15
16  // in order for the set-cookie header to be set,
17  // headers must be returned as part of the loader response
18  return json(
19    { data, error },
20    {
21      headers: response.headers,
22    }
23  )
24}

退出登录

1export const action = async ({ request }) => {
2  const { email, password } = Object.fromEntries(await request.formData())
3  const response = new Response()
4
5  const supabaseClient = createServerClient(
6    process.env.SUPABASE_URL,
7    process.env.SUPABASE_ANON_KEY,
8    { request, response }
9  )
10
11  const { error } = await supabaseClient.auth.signOut()
12
13  // in order for the set-cookie header to be set,
14  // headers must be returned as part of the loader response
15  return json(
16    { error },
17    {
18      headers: response.headers,
19    }
20  )
21}

订阅实时事件

1import { createBrowserClient } from '@supabase/auth-helpers-remix'
2import { useState, useEffect } from 'react'
3
4export default function SubscribeToRealtime() {
5  const [data, setData] = useState([])
6
7  useEffect(() => {
8    const supabaseClient = createBrowserClient(
9      window.env.SUPABASE_URL,
10      window.env.SUPABASE_ANON_KEY
11    )
12    const channel = supabaseClient
13      .channel('test')
14      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'test' }, (payload) => {
15        setData((data) => [...data, payload.new])
16      })
17      .subscribe()
18
19    return () => {
20      supabaseClient.removeChannel(channel)
21    }
22  }, [session])
23
24  return <pre>{JSON.stringify({ data }, null, 2)}</pre>
25}

注意:window.env不是由Remix自动填充的。请看上面的 客户端说明来配置它。

在这个例子中,我们要监听test表的INSERT事件。只要Supabase的test表有新行,我们的用户界面就会自动更新新数据。

在实时事件中合并服务器和客户端状态

1import { json, LoaderFunction } from '@remix-run/node';
2import { useLoaderData, useNavigate } from '@remix-run/react';
3import {
4  createServerClient,
5  createBrowserClient
6} from '@supabase/auth-helpers-remix';
7import { useEffect } from 'react';
8import { Database } from '../../db_types';
9
10// this route demonstrates how to subscribe to realtime updates
11// and synchronize data between server and client
12export const loader: LoaderFunction = async ({
13  request
14}: {
15  request: Request;
16}) => {
17  const response = new Response();
18  const supabaseClient = createServerClient<Database>(
19    process.env.SUPABASE_URL!,
20    process.env.SUPABASE_ANON_KEY!,
21    { request, response }
22  );
23
24  const {
25    data: { session }
26  } = await supabaseClient.auth.getSession();
27
28  const { data, error } = await supabaseClient.from('test').select('*');
29
30  if (error) {
31    throw error;
32  }
33
34  // in order for the set-cookie header to be set,
35  // headers must be returned as part of the loader response
36  return json(
37    { data, session },
38    {
39      headers: response.headers
40    }
41  );
42};
43
44export default function SubscribeToRealtime() {
45  const { data, session } = useLoaderData();
46  const navigate = useNavigate();
47
48  useEffect(() => {
49    // Note: window.env is not automatically populated by Remix
50    // Check out the [example in this repo](../root.tsx) or
51    // [Remix docs](https://remix.run/docs/en/v1/guides/envvars#browser-environment-variables) for more info
52    const supabaseClient = createBrowserClient<Database>(
53      window.env.SUPABASE_URL,
54      window.env.SUPABASE_ANON_KEY
55    );
56    // make sure you have enabled `Replication` for your table to receive realtime events
57    // https://supabase.com/docs/guides/database/replication
58    const channel = supabaseClient
59      .channel('test')
60      .on(
61        'postgres_changes',
62        { event: '*', schema: 'public', table: 'test' },
63        (payload: any) => {
64          // you could manually merge the `payload` with `data` here
65          // the `navigate` trick below causes all active loaders to be called again
66          // this handles inserts, updates and deletes, keeping everything in sync
67          // which feels more remix-y than manually merging state
68          // https://sergiodxa.com/articles/automatic-revalidation-in-remix
69          navigate('.', { replace: true });
70        }
71      )
72      .subscribe();
73
74    return () => {
75      supabaseClient.removeChannel(channel);
76    };
77  }, [session]);
78
79  return (
80    <div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
81      <pre>{JSON.stringify({ data }, null, 2)}</pre>
82    </div>
83  );
84}

注意:window.env不是由Remix自动填充的。请看上面的 客户端说明来配置它。

使用TypeScript的用法#

你可以将用Supabase CLI生成的类型传递给createServerClientcreateBrowserClient函数以获得增强的类型安全和自动完成。

服务器端

1import { createServerClient } from '@supabase/auth-helpers-remix'
2import { Database } from '../../db_types'
3
4export const loader = async ({ request }) => {
5  const response = new Response()
6
7  const supabaseClient = createServerClient<Database>(
8    process.env.SUPABASE_URL,
9    process.env.SUPABASE_ANON_KEY,
10    { request, response }
11  )
12}

客户端

1import { createBrowserClient } from '@supabase/auth-helpers-remix'
2import { Database } from '../../db_types'
3
4const supabaseClient = createBrowserClient<Database>(
5  process.env.SUPABASE_URL,
6  process.env.SUPABASE_ANON_KEY
7)

使用provider_token向OAuth APIs获取服务器端数据#

当使用第三方认证供应商时,会话是由一个额外的provider_token字段发起的,该字段被持久地保存在认证cookie中,可以在会话对象中访问。provider_token可以用来代表登录的用户向OAuth提供商的API端点发出API请求。

1import { json, LoaderFunction, redirect } from '@remix-run/node'; // change this import to whatever runtime you are using
2import { useLoaderData } from '@remix-run/react';
3import { createServerClient, User } from '@supabase/auth-helpers-remix';
4import { Database } from '../../db_types';
5
6export const loader: LoaderFunction = async ({
7  request
8}: {
9  request: Request;
10}) => {
11  const response = new Response();
12
13  const supabaseClient = createServerClient<Database>(
14    process.env.SUPABASE_URL!,
15    process.env.SUPABASE_ANON_KEY!,
16    { request, response }
17  );
18
19  const {
20    data: { session }
21  } = await supabaseClient.auth.getSession();
22
23  if (!session) {
24    // there is no session, therefore, we are redirecting
25    // to the landing page. we still need to return
26    // response.headers to attach the set-cookie header
27    return redirect('/', {
28      headers: response.headers
29    });
30  }
31
32  // Retrieve provider_token & logged in user's third-party id from metadata
33  const { provider_token, user } = session;
34  const userId = user.user_metadata.user_name;
35
36  const allRepos = await (
37    await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
38      method: 'GET',
39      headers: {
40        Authorization: `token ${provider_token}`
41      }
42    })
43  ).json();
44
45  // in order for the set-cookie header to be set,
46  // headers must be returned as part of the loader response
47  return json(
48    { user, allRepos },
49    {
50      headers: response.headers
51    }
52  );
53};
54
55export default function ProtectedPage() {
56  // by fetching the user in the loader, we ensure it is available
57  // for first SSR render - no flashing of incorrect state
58  const { user, allRepos } = useLoaderData<{ user: User; allRepos: any }>();
59
60  return <pre>{JSON.stringify({ user, allRepos }, null, 2)}</pre>;
61}