如何在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
相关产品推荐
相关产品推荐

