Supabase Remix认证
该子模块为在Remix应用程序中实现用户身份验证提供了方便的帮助。
安装Remix助手库#
1npm install @supabase/auth-helpers-remix
此库支持以下工具版本:
- Remix:
>=1.7.2
设置环境变量
在项目的API设置中检索项目URL和匿名密钥以设置以下环境变量。对于本地开发,您可以将其设置为.env
文件。参见示例.
.envSUPABASE_URL=YOUR_SUPABASE_URL SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
加载器
加载器函数在组件呈现之前立即在服务器上运行。它们响应路由上的所有GET请求。您可以通过调用 createServerClient
函数并将SUPABASE_URL
、SUPABASE_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_URL
、SUPABASE_ANON_KEY
以及Request
和Response
传递给它来创建一个经过认证的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.jsx1export 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.jsx1const { env } = useLoaderData()
然后,添加一个<script>
标签,将这些环境变量附加到window
中。这应该被放在app/root.jsx
中的<Scripts />
组件之前。
app/root.jsx1<script
2 dangerouslySetInnerHTML={{
3 __html: `window.env = ${JSON.stringify(env)}`,
4 }}
5/>
Node的完整例子:
app/root.jsx1import { 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 />47484950
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
的支持的认证策略都可以在服务器端工作。这就是你如何处理简单的 email
和 password
认证。
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
的支持的认证策略都可以在服务器端工作。这就是你如何处理简单的 email
和 password
认证。
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生成的类型传递给createServerClient
或createBrowserClient
函数以获得增强的类型安全和自动完成。
服务器端
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}