如何在浏览器扩展中实现 Google OAuth 而无需 “Tabs” 权限

发布: (2025年12月7日 GMT+8 20:15)
6 min read
原文: Dev.to

Source: Dev.to

架构:工作原理

我们并不在扩展内部进行认证(因为弹出页可能在流程中途关闭)。
相反,我们在自己的网站上处理认证,并将会话令牌传递给扩展。

流程

  1. 用户安装扩展后被重定向到你的 Web 应用的注册页面。
  2. 用户在 Web 应用上使用 Google OAuth 或邮箱/密码进行认证。
  3. 认证成功后,令牌会以隐藏的 DOM 元素形式暴露出来。
  4. 在你的 Web 应用上运行的内容脚本会提取这些令牌。
  5. 令牌被发送到扩展的后台脚本。
  6. 后台脚本创建会话并将其本地存储。
  7. 扩展和 Web 应用现在保持同步。

设置 Web 应用

首先,在你的 Next.js(或 React + Vite)项目中创建一个认证上下文,用来管理用户状态。下面我们使用 Supabase 的 onAuthStateChange 监听器来检测用户登录。

步骤 1:认证上下文

创建 contexts/AuthContext.tsx。该提供者管理用户会话并在整个应用中提供。

// contexts/AuthContext.tsx
'use client';

import React, { createContext, useContext, useEffect, useState } from 'react';
import supabase from '@/lib/supabase/supabaseClient';

type AuthContextType = {
  user: SupabaseUser | undefined;
  session: SupabaseSession | undefined;
  loading: boolean;
  logout: () => Promise;
};

type AuthProviderProp = {
  children: React.ReactNode;
};

const AuthContext = createContext(undefined);

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

export function AuthProvider({ children }: AuthProviderProp) {
  const [user, setUser] = useState(undefined);
  const [session, setSession] = useState(undefined);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    supabase.auth.onAuthStateChange((eventName, session) => {
      console.log('Auth state changed:', eventName, session);

      if (eventName === 'INITIAL_SESSION' && session?.user) {
        setUser(session.user);
        setSession(session);
      } else if (eventName === 'SIGNED_IN' && session?.user) {
        setUser(session.user);
        setSession(session);
      } else if (eventName === 'SIGNED_OUT') {
        setUser(undefined);
        setSession(undefined);
      }
    });
  }, []);

  const value = {
    user,
    session,
    loading,
    logout: () => supabase.auth.signOut(),
  };

  return {children};
}

onAuthStateChange 监听器会在认证状态变化时触发,帮助我们在用户成功登录后更新 UI 并暴露令牌。

步骤 2:为应用套上包装

在你的应用(例如 app/layout.tsx)外层包裹 AuthProvider,使整个应用都能访问用户状态。

// app/layout.tsx
import { AuthProvider } from '@/contexts/AuthContext';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
      
        {children}
      
    
  );
}

步骤 3:创建认证页面

编写用户注册或登录的页面。

// app/auth/page.tsx
'use client';

import { useState } from 'react';
import supabase from '@/lib/supabase/supabaseClient';

export default function AuthPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);

  async function handleRegister() {
    setLoading(true);
    const { data, error } = await supabase.auth.signUp({
      email: email.trim(),
      password,
      options: {
        emailRedirectTo: process.env.NEXT_PUBLIC_AUTH_SUCCESS_URL,
      },
    });
    setLoading(false);

    if (error) {
      console.error('Registration error:', error);
      // Handle error (show toast, etc.)
    }
  }

  async function handleGoogleLogin() {
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: process.env.NEXT_PUBLIC_AUTH_SUCCESS_URL,
        queryParams: {
          access_type: 'offline',
          prompt: 'consent',
        },
      },
    });
  }

  return (
    
      
## Sign Up for Your Extension

       setEmail(e.target.value)}
        placeholder="Email"
      />

       setPassword(e.target.value)}
        placeholder="Password"
      />

      
        Sign Up with Email
      

      Continue with Google
    
  );
}

步骤 4:通过隐藏的 DOM 元素暴露令牌

在页面(例如页脚)中加入隐藏元素,存放认证令牌。扩展的内容脚本会读取这些元素。

// components/Footer.tsx
'use client';

import { useAuth } from '@/contexts/AuthContext';

const Footer = () => {
  const { session, user } = useAuth();

  return (
    <>
      {/* Hidden elements for the extension's content script */}
      {session && (
        
          {user?.email}
          {session?.access_token}
          {session?.refresh_token}
        
      )}

      {/* Your regular footer content */}
      {/* Footer content */}
    
  );
};

export default Footer;

sr-only 类(仅屏幕阅读器可见)使这些元素在视觉上不可见,但仍然存在于 DOM 中,供扩展访问。

构建 Chrome 扩展

步骤 5:创建内容脚本以提取令牌

内容脚本在你的 Web 应用域名下运行,监听隐藏的令牌元素并将令牌转发给后台脚本。下面是使用 WXT 框架的示例(你也可以改写为普通的内容脚本)。

// authContentScript.ts
export default defineContentScript({
  matches: ['https://your-web-app.com/*'], // Replace with your domain
  allFrames: true,
  runAt: 'document_idle',
  main() {
    console.log('Auth content script initialized');

    function observeAuthTokens() {
      const observer = new MutationObserver(() => {
        const accessTokenEl = document.querySelector('.accessTokenDiv');
        const refreshTokenEl = document.querySelector('.refreshTokenDiv');
        const emailEl = document.querySelector('.userEmailDiv');

        if (accessTokenEl && refreshTokenEl && emailEl) {
          const tokens = {
            accessToken: accessTokenEl.textContent,
            refreshToken: refreshTokenEl.textContent,
            email: emailEl.textContent,
          };
          // Send tokens to background script
          chrome.runtime.sendMessage({ type: 'SET_TOKENS', payload: tokens });
        }
      });

      observer.observe(document.body, { childList: true, subtree: true });
    }

    observeAuthTokens();
  },
});

脚本监视 DOM 变化,在令牌出现时提取其值,并通过 chrome.runtime.sendMessage 将其发送给扩展的后台脚本,随后可以使用 storage 权限将会话存储下来。

Back to Blog

相关文章

阅读更多 »

Payload 中的自定义 auth

Payload 中自定义身份验证的封面图像 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads...