Supabase OAuth集成Remix时重定向警告及认证失效问题
问题描述
- 使用Supabase Auth完成第三方(GitHub)身份登录后,无法查看仅认证用户可见的数据(已配置RLS规则)
- 控制台触发警告:
You are trying to use a blocker on a POP navigation to a location that was not created by @remix-run/router. This will fail silently in production. This can happen if you are navigating outside the router via
window.history.pushState/window.location.hashinstead of using router navigation APIs. This can also happen if you are using createHashRouter and the user manually changes the URL.
相关代码片段:
// login.tsx import { useOutletContext } from '@remix-run/react'; import type { SupabaseOutletContext } from '~/root'; export function Login() { const { supabase } = useOutletContext<SupabaseOutletContext>(); async function handleLogin() { return (await supabase).auth.signInWithOAuth({ provider: 'github', }); } const handleLogout = async () => { const { error } = await supabase.auth.signOut(); if (error) { console.log(error); } }; return ( <div> <h1>Login</h1> <button onClick={handleLogin}>Login</button> <button onClick={handleLogout}>Logout</button> </div> ); }
// root.tsx // ... import { createClient } from '@supabase/supabase-js'; import type { SupabaseClient } from '@supabase/supabase-js'; // ... type TypedSupabaseClient = SupabaseClient<Database>; export type SupabaseOutletContext = { supabase: TypedSupabaseClient; }; // ... export const loader = ({}) => { const env = { SUPABASE_URL: process.env.SUPABASE_URL!, SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!, }; return json({ env }); }; export default function App() { const { env } = useLoaderData(); const [supabase] = useState(() => createClient<Database>(env.SUPABASE_URL!, env.SUPABASE_ANON_KEY!) ); return ( {/* ... */} <Outlet context={{ supabase }} /> {/* ... */} ); }
// index.tsx import type { LoaderArgs } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import { Login } from 'components/login'; import supabase from 'utils/supabase.server'; export const loader = async ({}) => { // uses RLS const { data } = await supabase.from('messages').select('*'); return { messages: data ?? [] }; }; export default function Index() { const { messages } = useLoaderData(); return ( <> <Login /> <pre>{JSON.stringify({ messages }, null, 2)}</pre> </> ); }
使用包版本:
"@remix-run/node": "1.12.0", "@remix-run/react": "1.12.0", "react": "18.2.0", "react-dom": "18.2.0", "@supabase/supabase-js": "^2.7.1"
原因分析
控制台警告原因
Supabase OAuth默认通过直接修改window.location完成页面跳转,没有使用Remix内置的路由导航API。Remix的路由拦截器(blocker)无法识别这种非路由系统创建的导航记录,因此触发警告。
认证失效原因
服务端loader中使用的supabase.server客户端没有携带用户的认证会话信息:
- 浏览器端Supabase会自动存储会话Cookie,但服务端无法直接读取,需要手动从请求头中提取会话并传递给服务端Supabase客户端
- 服务端未获取到用户会话,RLS规则判定用户未认证,因此拦截了数据查询
解决方法
1. 修复OAuth跳转逻辑,消除控制台警告
指定OAuth回调路由,用Remix的路由API处理跳转,避免直接修改window.location:
修改login.tsx:
import { useOutletContext, useNavigate } from '@remix-run/react'; import type { SupabaseOutletContext } from '~/root'; export function Login() { const { supabase } = useOutletContext<SupabaseOutletContext>(); const navigate = useNavigate(); async function handleLogin() { const { error } = await supabase.auth.signInWithOAuth({ provider: 'github', options: { redirectTo: `${window.location.origin}/callback` } }); if (error) console.error(error); } const handleLogout = async () => { const { error } = await supabase.auth.signOut(); if (error) console.log(error); navigate('/', { replace: true }); }; return ( <div> <h1>Login</h1> <button onClick={handleLogin}>Login</button> <button onClick={handleLogout}>Logout</button> </div> ); }
新增app/routes/callback.tsx处理OAuth回调:
import { LoaderArgs, redirect } from '@remix-run/node'; import { createClient } from '@supabase/supabase-js'; import type { Database } from '~/types/supabase'; export const loader = async ({ request }: LoaderArgs) => { const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY! ); const url = new URL(request.url); const code = url.searchParams.get('code'); if (code) { await supabase.auth.exchangeCodeForSession(code); } return redirect('/'); };
2. 传递服务端会话,让RLS规则生效
修改utils/supabase.server.ts,从请求头中提取会话:
import { createClient } from '@supabase/supabase-js'; import type { Database } from '~/types/supabase'; import type { Request } from '@remix-run/node'; export function createSupabaseServerClient(request: Request) { const supabase = createClient<Database>( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY! ); const cookies = request.headers.get('Cookie'); if (cookies) { const refreshToken = cookies.split('refresh_token=')[1]?.split(';')[0]; if (refreshToken) { supabase.auth.setSession({ refresh_token: refreshToken }); } } return supabase; }
修改index.tsx的loader,传入request创建带会话的服务端客户端:
import type { LoaderArgs } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import { Login } from 'components/login'; import { createSupabaseServerClient } from 'utils/supabase.server'; export const loader = async ({ request }: LoaderArgs) => { const supabase = createSupabaseServerClient(request); const { data } = await supabase.from('messages').select('*'); return { messages: data ?? [] }; }; export default function Index() { const { messages } = useLoaderData(); return ( <> <Login /> <pre>{JSON.stringify({ messages }, null, 2)}</pre> </> ); }
3. 优化:监听会话变化自动刷新页面
在root.tsx中添加会话监听,确保用户登录/登出后页面自动更新:
import { useEffect } from 'react'; import { useOutletContext, useNavigate } from '@remix-run/react'; // ... export default function App() { const { env } = useLoaderData(); const navigate = useNavigate(); const [supabase] = useState(() => createClient<Database>(env.SUPABASE_URL!, env.SUPABASE_ANON_KEY!) ); useEffect(() => { const { data: authListener } = supabase.auth.onAuthStateChange((event) => { if (event === 'SIGNED_IN' || event === 'SIGNED_OUT') { navigate(window.location.pathname, { replace: true }); } }); return () => authListener.unsubscribe(); }, [supabase, navigate]); return ( {/* ... */} <Outlet context={{ supabase }} /> {/* ... */} ); }
内容的提问来源于stack exchange,提问作者Aleksander Jess

