You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

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 }
  );
};
问题分析与解决方案

核心问题点

  1. 登录前的无效signOut操作:在新登录场景下,当前请求中并没有有效会话,调用await supabase.auth.signOut()会尝试清除不存在的刷新令牌Cookie,干扰后续登录时的Cookie设置流程,导致刷新令牌无法正确存储。
  2. 环境变量重复加载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

火山引擎 最新活动