Remix.js+Supabase刷新令牌失效求助:需重启服务器才能登录
问题描述
使用Remix.js开发Web应用,Supabase作为BaaS服务。默认access token1小时过期,新浏览器无Cookie登录或登出后再登录,access token过期时会抛出AuthApiError: Invalid Refresh Token: Refresh Token Not Found错误,必须重启服务器才能再次登录。已关闭Supabase控制台的“检测并撤销潜在风险的刷新令牌”选项,问题仍存在。
相关代码片段
admin/login路由的action函数
import { getSupabaseServiceClient } from "supabase/supabase.server"; import { useActionData } from "@remix-run/react"; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const validatedFormData = await adminLoginFormValidator.validate(formData); if (validatedFormData.error) { return { type: "Error", message: validatedFormData.error.fieldErrors[0], } as NotificationProps; } const { email, password } = validatedFormData.data; const response = new Response(); const supabase = getSupabaseServiceClient({ request: request, response: response, }); // Clear any stale session before login await supabase.auth.signOut(); const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { return { type: "Error", message: error.message, } as NotificationProps; } else { return redirect("/admin", { headers: response.headers, // this updates the session cookie }); } };
supabase.server.ts函数
import { createServerClient } from "@supabase/auth-helpers-remix"; import { config } from "dotenv"; export const getSupabaseServiceClient = ({ request, response, }: { request: Request; response: Response; }) => { config(); return createServerClient( process.env.SUPABASE_URL || "", process.env.SUPABASE_ANON_KEY || "", { request, response } ); };
问题分析与解决方案
核心问题点
- 登录前的无效signOut操作:在新登录场景下,当前请求中并没有有效会话,调用
await supabase.auth.signOut()会尝试清除不存在的刷新令牌Cookie,干扰后续登录时的Cookie设置流程,导致刷新令牌无法正确存储。 - 环境变量重复加载:
supabase.server.ts中每次创建客户端都执行config(),重复加载环境变量,在生产环境可能引发不可预期的状态问题。
修复步骤
1. 移除登录前不必要的signOut调用
删除await supabase.auth.signOut();代码行,新登录时用户无有效会话,该操作完全多余且会破坏Cookie流程。修改后的action函数:
import { getSupabaseServiceClient } from "supabase/supabase.server"; import { useActionData } from "@remix-run/react"; export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const validatedFormData = await adminLoginFormValidator.validate(formData); if (validatedFormData.error) { return { type: "Error", message: validatedFormData.error.fieldErrors[0], } as NotificationProps; } const { email, password } = validatedFormData.data; const response = new Response(); const supabase = getSupabaseServiceClient({ request: request, response: response, }); const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { return { type: "Error", message: error.message, } as NotificationProps; } else { return redirect("/admin", { headers: response.headers, // this updates the session cookie }); } };
2. 修正环境变量加载逻辑
将config()移到文件顶部,仅执行一次,避免重复加载:
import { createServerClient } from "@supabase/auth-helpers-remix"; import { config } from "dotenv"; // 仅在文件初始化时加载一次环境变量 config(); export const getSupabaseServiceClient = ({ request, response, }: { request: Request; response: Response; }) => { return createServerClient( process.env.SUPABASE_URL || "", process.env.SUPABASE_ANON_KEY || "", { request, response } ); };
额外优化建议
- 提前处理会话过期:在admin路由的loader中检查会话有效性,当access token即将过期时主动触发刷新,避免错误抛出:
export async function loader({ request }: LoaderFunctionArgs) { const response = new Response(); const supabase = getSupabaseServiceClient({ request, response }); const { data: { session }, error } = await supabase.auth.getSession(); if (error || !session) { return redirect("/admin/login", { headers: response.headers }); } // 剩余时间小于1分钟时刷新会话 const expiresAt = session.expires_at * 1000; if (expiresAt - Date.now() < 60 * 1000) { const { data: refreshedSession } = await supabase.auth.refreshSession(); if (refreshedSession.session) { return json({ session: refreshedSession.session }, { headers: response.headers }); } } return json({ session }, { headers: response.headers }); }
- 区分错误类型:登录逻辑中针对不同Auth错误返回更精准的提示,比如区分邮箱不存在、密码错误等情况。
- 敏感操作使用服务角色密钥:管理员级别的敏感操作,单独创建使用服务角色密钥的Supabase客户端,避免匿名密钥暴露过多权限。
内容的提问来源于stack exchange,提问作者Rooty Joe




