本文档介绍了如何使用 Firebase Authentication 让用户登录使用 Manifest V3 的 Chrome 扩展程序。
Firebase Authentication 提供多种身份验证方法,以便用户通过 Chrome 扩展程序登录,其中有些方法需要比其他方法更多的开发工作。
如需在 Manifest V3 Chrome 扩展程序中使用以下方法,只需从 firebase/auth/web-extension
导入这些方法:
- 使用电子邮件和密码(
createUserWithEmailAndPassword
和signInWithEmailAndPassword
)登录 - 使用电子邮件链接(
sendSignInLinkToEmail
、isSignInWithEmailLink
和signInWithEmailLink
)登录 - 匿名登录 (
signInAnonymously
) - 使用自定义身份验证系统 (
signInWithCustomToken
) 登录 - 独立处理提供方登录流程,然后使用
signInWithCredential
系统还支持以下登录方法,但需要执行一些额外的操作:
- 通过弹出式窗口(
signInWithPopup
、linkWithPopup
和reauthenticateWithPopup
)登录 - 重定向至登录页面(
signInWithRedirect
、linkWithRedirect
和reauthenticateWithRedirect
)进行登录 - 使用电话号码和 reCAPTCHA 登录
- 使用 reCAPTCHA 的短信多重身份验证
- reCAPTCHA Enterprise 保护
如需在 Manifest V3 Chrome 扩展程序中使用这些方法,您必须使用屏幕外文档。
使用 firebase/auth/web-extension 入口点
通过从 firebase/auth/web-extension
导入,可使用户通过 Chrome 扩展程序登录与 Web 应用类似。
只有 Web SDK v10.8.0 版及更高版本支持 firebase/auth/web-extension。
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth/web-extension'; const auth = getAuth(); signInWithEmailAndPassword(auth, email, password) .then((userCredential) => { // Signed in const user = userCredential.user; // ... }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; });
使用屏幕外文档
某些身份验证方法(例如 signInWithPopup
、linkWithPopup
和 reauthenticateWithPopup
)与 Chrome 扩展程序不直接兼容,因为它们要求从扩展程序软件包外部加载代码。从 Manifest V3 开始,这是不允许的,将受到扩展程序平台阻止。为了解决此问题,您可以使用屏幕外文档在 iframe 中加载该代码。在屏幕外文档中,实现常规身份验证流程,并将屏幕外文档的结果代理回扩展程序。
本指南以 signInWithPopup
为例,但同样的概念也适用于其他身份验证方法。
准备工作
此方法要求您设置一个可在网络上加载的网页,该网页将在 iframe 中加载。任何主机都可以做到这一点,包括 Firebase Hosting。创建一个包含以下内容的网站:
<!DOCTYPE html> <html> <head> <title>signInWithPopup</title> <script src="signInWithPopup.js"></script> </head> <body><h1>signInWithPopup</h1></body> </html>
联合登录
如果您使用的是联合登录(例如使用 Google、Apple、SAML 或 OIDC 账号登录),则必须将您的 Chrome 扩展程序 ID 添加到已获授权的网域列表中:
- 在 Firebase 控制台中打开您的项目。
- 在身份验证部分中,打开设置页面。
- 向“已获授权的网域”列表添加格式如下的 URI:
chrome-extension://CHROME_EXTENSION_ID
在 Chrome 扩展程序的清单文件中,确保将以下网址添加到 content_security_policy
许可名单中:
https://apis.google.com
https://www.gstatic.com
https://www.googleapis.com
https://securetoken.googleapis.com
实现身份验证
在 HTML 文档中,signInWithPopup.js 是处理身份验证的 JavaScript 代码。您可以通过以下两种方式实现扩展程序中直接支持的方法:
- 使用
firebase/auth
,而不是firebase/auth/web-extension
。web-extension
入口点适用于在扩展程序中运行的代码。虽然此代码最终在扩展程序中运行(在 iframe 中或屏幕外文档中),但其运行所在环境是标准 Web。 - 将身份验证逻辑封装在
postMessage
监听器中,以便代理身份验证请求和响应。
import { signInWithPopup, GoogleAuthProvider, getAuth } from'firebase/auth'; import { initializeApp } from 'firebase/app'; import firebaseConfig from './firebaseConfig.js' const app = initializeApp(firebaseConfig); const auth = getAuth(); // This code runs inside of an iframe in the extension's offscreen document. // This gives you a reference to the parent frame, i.e. the offscreen document. // You will need this to assign the targetOrigin for postMessage. const PARENT_FRAME = document.location.ancestorOrigins[0]; // This demo uses the Google auth provider, but any supported provider works. // Make sure that you enable any provider you want to use in the Firebase Console. // https://console.firebase.google.com/project/_/authentication/providers const PROVIDER = new GoogleAuthProvider(); function sendResponse(result) { globalThis.parent.self.postMessage(JSON.stringify(result), PARENT_FRAME); } globalThis.addEventListener('message', function({data}) { if (data.initAuth) { // Opens the Google sign-in page in a popup, inside of an iframe in the // extension's offscreen document. // To centralize logic, all respones are forwarded to the parent frame, // which goes on to forward them to the extension's service worker. signInWithPopup(auth, PROVIDER) .then(sendResponse) .catch(sendResponse) } });
构建 Chrome 扩展程序
您的网站发布后,您可以在 Chrome 扩展程序中使用该网站。
- 向 manifest.json 文件添加
offscreen
权限: - 创建屏幕外文档。这是扩展程序软件包内的一个极小的 HTML 文件,用于加载屏幕外文档 JavaScript 的逻辑:
- 在扩展程序软件包中添加
offscreen.js
。这用于充当第 1 步中设置的公共网站与您的扩展程序之间的代理。 - 通过 background.js Service Worker 设置屏幕外文档。
{ "name": "signInWithPopup Demo", "manifest_version" 3, "background": { "service_worker": "background.js" }, "permissions": [ "offscreen" ] }
<!DOCTYPE html> <script src="./offscreen.js"></script>
// This URL must point to the public site const _URL = 'https://example.com/signInWithPopupExample'; const iframe = document.createElement('iframe'); iframe.src = _URL; document.documentElement.appendChild(iframe); chrome.runtime.onMessage.addListener(handleChromeMessages); function handleChromeMessages(message, sender, sendResponse) { // Extensions may have an number of other reasons to send messages, so you // should filter out any that are not meant for the offscreen document. if (message.target !== 'offscreen') { return false; } function handleIframeMessage({data}) { try { if (data.startsWith('!_{')) { // Other parts of the Firebase library send messages using postMessage. // You don't care about them in this context, so return early. return; } data = JSON.parse(data); self.removeEventListener('message', handleIframeMessage); sendResponse(data); } catch (e) { console.log(`json parse failed - ${e.message}`); } } globalThis.addEventListener('message', handleIframeMessage, false); // Initialize the authentication flow in the iframed document. You must set the // second argument (targetOrigin) of the message in order for it to be successfully // delivered. iframe.contentWindow.postMessage({"initAuth": true}, new URL(_URL).origin); return true; }
const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; // A global promise to avoid concurrency issues let creatingOffscreenDocument; // Chrome only allows for a single offscreenDocument. This is a helper function // that returns a boolean indicating if a document is already active. async function hasDocument() { // Check all windows controlled by the service worker to see if one // of them is the offscreen document with the given path const matchedClients = await clients.matchAll(); return matchedClients.some( (c) => c.url === chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH) ); } async function setupOffscreenDocument(path) { // If we do not have a document, we are already setup and can skip if (!(await hasDocument())) { // create offscreen document if (creating) { await creating; } else { creating = chrome.offscreen.createDocument({ url: path, reasons: [ chrome.offscreen.Reason.DOM_SCRAPING ], justification: 'authentication' }); await creating; creating = null; } } } async function closeOffscreenDocument() { if (!(await hasDocument())) { return; } await chrome.offscreen.closeDocument(); } function getAuth() { return new Promise(async (resolve, reject) => { const auth = await chrome.runtime.sendMessage({ type: 'firebase-auth', target: 'offscreen' }); auth?.name !== 'FirebaseError' ? resolve(auth) : reject(auth); }) } async function firebaseAuth() { await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH); const auth = await getAuth() .then((auth) => { console.log('User Authenticated', auth); return auth; }) .catch(err => { if (err.code === 'auth/operation-not-allowed') { console.error('You must enable an OAuth provider in the Firebase' + ' console in order to use signInWithPopup. This sample' + ' uses Google by default.'); } else { console.error(err); return err; } }) .finally(closeOffscreenDocument) return auth; }
现在,当您在 Service Worker 中调用 firebaseAuth()
时,它会创建屏幕外文档并在 iframe 中加载网站。该 iframe 将在后台处理,并且 Firebase 会经历标准身份验证流程。待解析或拒绝后,系统将使用屏幕外文档将身份验证对象从 iframe 代理到 Service Worker。