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

如何在Next.js应用与Plasmo Chrome扩展间同步Supabase会话?

Next.js + Plasmo扩展 + Supabase 会话同步问题解决方案

问题背景

使用Plasmo开发Chrome扩展,同时基于Next.js(App Router)开发Web应用,通过Supabase实现认证功能,需要保持Web应用与扩展间的认证会话同步。当前通过Web应用登录后,用chrome.runtime.sendMessage将会话发送至扩展,扩展后台脚本调用supabase.auth.setSession设置会话,登录时可正常工作,但扩展偶尔会随机登出。

当前实现代码

Web应用端(向扩展发送会话)

"use client";

import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";

export default function SendSessionToExtension() {
  useEffect(() => {
    const supabase = createClient();

    const { data: listener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        if (!chrome.runtime) return;

        const crxId = process.env.NEXT_PUBLIC_CRX_ID;
        if (!crxId) return;

        chrome.runtime.sendMessage(
          crxId,
          { type: "SYNC_SESSION", event, session },
          (response) => {
            if (chrome.runtime.lastError) {
              console.error("Error sending message:", chrome.runtime.lastError);
            } else {
              console.log("Message sent successfully:", response);
            }
          }
        );
      }
    );

    return () => listener.subscription.unsubscribe();
  }, []);

  return null;
}

扩展后台脚本(接收并设置会话)

import { supabase } from "~core/supabase";

chrome.runtime.onMessageExternal.addListener(
  async (message, sender, sendResponse) => {
    if (message.type === "SYNC_SESSION") {
      if (message.session) {
        await supabase.auth.setSession({
          access_token: message.session.access_token,
          refresh_token: message.session.refresh_token
        });

        chrome.storage.local.set({ user: message.session });
        sendResponse({ success: true });
      } else {
        await supabase.auth.signOut();
        chrome.storage.local.remove("user");
        sendResponse({ success: true });
      }
    }
    return true;
  }
);

遇到的问题

  • 扩展偶尔出现随机登出
  • 不确定当前会话同步方式的正确性
  • 怀疑Cookie或后台服务工作者行为导致问题

核心疑问解答

1. 正确的会话同步方式

当前单向同步(Web→扩展)存在局限性,需结合双向同步+持久化兜底:

  • 扩展启动时,优先通过supabase.auth.getSession()恢复会话,若失败则从chrome.storage.local读取备份会话调用setSession恢复
  • Web端和扩展端都监听supabase.auth.onAuthStateChange,会话变化时互相同步(需添加判断逻辑避免循环触发)
  • 为Supabase客户端配置Chrome扩展专属的存储适配器,替代默认的localStorage,避免环境差异导致的会话丢失

2. 令牌刷新的处理

Supabase客户端默认自动处理令牌刷新,但扩展环境需注意:

  • 显式配置autoRefreshToken: true和persistSession: true
  • 扩展后台脚本被Chrome休眠时,可能导致自动刷新失败,需在扩展启动时检查会话有效性,若过期且存在刷新令牌,手动触发刷新
  • 保存会话时同步存储expires_at,定期检查并提前触发刷新

3. Cookie与权限配置

  • 权限配置:在扩展manifest.json中添加"storage"权限(用于会话持久化),以及Supabase域名的host_permissions,例如:
    {
      "permissions": ["storage"],
      "host_permissions": ["https://<your-supabase-url>/*"]
    }
    
  • Cookie问题:扩展环境不依赖Cookie同步会话,优先使用chrome.storage.local作为Supabase存储介质,避免Chrome Cookie隔离策略影响

优化后的代码示例

扩展端Supabase客户端配置(~core/supabase.js)

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.PLASMO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.PLASMO_PUBLIC_SUPABASE_ANON_KEY;

// 适配Chrome扩展的存储适配器
const chromeStorageAdapter = {
  getItem: async (key) => {
    const result = await chrome.storage.local.get(key);
    return result[key] || null;
  },
  setItem: async (key, value) => {
    await chrome.storage.local.set({ [key]: value });
  },
  removeItem: async (key) => {
    await chrome.storage.local.remove(key);
  }
};

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: chromeStorageAdapter,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false // 扩展无需从URL检测会话
  }
});

扩展后台脚本优化

import { supabase } from "~core/supabase";

// 扩展启动时恢复会话
async function restoreSession() {
  const { data: { session } } = await supabase.auth.getSession();
  if (!session) {
    const savedSession = await chrome.storage.local.get('user');
    if (savedSession.user) {
      await supabase.auth.setSession(savedSession.user);
    }
  }
}

restoreSession();

// 监听扩展内部会话变化,同步到Web应用
supabase.auth.onAuthStateChange(async (event, session) => {
  const tabs = await chrome.tabs.query({ url: "https://<your-web-app-url>/*" });
  tabs.forEach(tab => {
    chrome.tabs.sendMessage(tab.id, { type: "SYNC_SESSION_FROM_EXT", event, session });
  });
});

chrome.runtime.onMessageExternal.addListener(
  async (message, sender, sendResponse) => {
    if (message.type === "SYNC_SESSION") {
      try {
        if (message.session) {
          await supabase.auth.setSession({
            access_token: message.session.access_token,
            refresh_token: message.session.refresh_token
          });
          await chrome.storage.local.set({ user: message.session });
          sendResponse({ success: true });
        } else {
          await supabase.auth.signOut();
          await chrome.storage.local.remove("user");
          sendResponse({ success: true });
        }
      } catch (error) {
        console.error("会话同步失败:", error);
        sendResponse({ success: false, error: error.message });
      }
    }
    return true; // 异步响应需返回true
  }
);

Web应用端优化(避免循环同步)

"use client";

import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";

export default function SyncSessionWithExtension() {
  useEffect(() => {
    const supabase = createClient();

    // 监听来自扩展的会话同步
    const handleExtensionMessage = (message, sender, sendResponse) => {
      if (message.type === "SYNC_SESSION_FROM_EXT") {
        supabase.auth.getSession().then(({ data: { session } }) => {
          if (JSON.stringify(session) !== JSON.stringify(message.session)) {
            message.session 
              ? supabase.auth.setSession(message.session) 
              : supabase.auth.signOut();
          }
        });
        sendResponse({ success: true });
      }
      return true;
    };

    chrome.runtime.onMessage.addListener(handleExtensionMessage);

    const { data: listener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        if (!chrome.runtime || !process.env.NEXT_PUBLIC_CRX_ID) return;

        // 避免重复发送相同会话
        const extensionSession = await chrome.storage.local.get('user');
        if (JSON.stringify(session) !== JSON.stringify(extensionSession.user)) {
          chrome.runtime.sendMessage(
            process.env.NEXT_PUBLIC_CRX_ID,
            { type: "SYNC_SESSION", event, session },
            (response) => {
              if (chrome.runtime.lastError) {
                console.error("发送会话到扩展失败:", chrome.runtime.lastError);
              }
            }
          );
        }
      }
    );

    return () => {
      listener.subscription.unsubscribe();
      chrome.runtime.onMessage.removeListener(handleExtensionMessage);
    };
  }, []);

  return null;
}

内容的提问来源于stack exchange,提问作者Renato Dinis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:24:55