You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Supabase OAuth集成Remix时重定向警告及认证失效问题

问题描述

  1. 使用Supabase Auth完成第三方(GitHub)身份登录后,无法查看仅认证用户可见的数据(已配置RLS规则)
  2. 控制台触发警告:

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.hash instead 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 17:05:22