如何在Chrome扩展MV3中实现并使用SSO登录?
在Chrome扩展Manifest V3中实现SSO登录的可行方案
Chrome MV3对扩展的权限和运行环境做了严格限制,但依然可以通过Chrome官方提供的chrome.identityAPI结合OAuth 2.0授权码流程(带PKCE)实现SSO登录,以下是具体实现方案:
核心思路
MV3中禁止了持久化后台页面,且CSP规则更严格,因此推荐使用弹出式Web授权流(通过chrome.identity.launchWebAuthFlow)结合PKCE(Proof Key for Code Exchange)来完成SSO登录,这种方式既符合MV3的安全要求,又能避免跨域问题。
具体实现步骤
1. 配置Manifest.json
首先在扩展的manifest.json中添加OAuth 2.0相关配置、必要的权限和回调地址:
{ "manifest_version": 3, "name": "SSO Demo Extension", "version": "1.0", "oauth2": { "client_id": "YOUR_CLIENT_ID", // 替换为你的SSO提供商客户端ID "scopes": ["openid", "email", "profile"], // 根据需求调整权限范围 "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", // 替换为对应SSO的授权端点 "token_endpoint": "https://oauth2.googleapis.com/token", // 替换为对应SSO的令牌端点 "redirect_uri": "https://<EXTENSION_ID>.chromiumapp.org/" // 固定格式,替换为你的扩展ID }, "host_permissions": [ "https://accounts.google.com/*", // 替换为SSO提供商的授权域名 "https://oauth2.googleapis.com/*" // 替换为SSO提供商的令牌域名 ], "action": { "default_popup": "popup.html" }, "background": { "service_worker": "background.js" }, "permissions": ["identity", "storage"] }
注意:
redirect_uri必须使用https://<EXTENSION_ID>.chromiumapp.org/格式,这是Chrome扩展专用的回调地址,可通过chrome.identity.getRedirectURL()自动获取。
2. 触发登录流程(Popup/Content Script)
在扩展的弹出页面或内容脚本中,通过chrome.identity.launchWebAuthFlow打开SSO登录窗口,并处理授权码:
// popup.js document.getElementById('login-btn').addEventListener('click', async () => { try { const manifest = chrome.runtime.getManifest(); const redirectUrl = chrome.identity.getRedirectURL(); // 生成PKCE验证所需的挑战码和验证器 const codeVerifier = generateRandomString(32); const codeChallenge = await generatePKCEChallenge(codeVerifier); // 构造授权URL const authUrl = new URL(manifest.oauth2.authorization_endpoint); authUrl.searchParams.set('client_id', manifest.oauth2.client_id); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', manifest.oauth2.scopes.join(' ')); authUrl.searchParams.set('redirect_uri', redirectUrl); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); // 启动Web授权流 const authResponse = await chrome.identity.launchWebAuthFlow({ url: authUrl.toString(), interactive: true }); // 从回调URL中提取授权码 const code = new URL(authResponse).searchParams.get('code'); // 发送授权码到后台脚本交换令牌 chrome.runtime.sendMessage( { type: 'exchangeToken', code, codeVerifier }, (response) => { if (response.success) { console.log('登录成功,令牌已获取'); chrome.storage.local.set({ accessToken: response.token }); // 更新UI状态 document.getElementById('login-btn').style.display = 'none'; document.getElementById('logout-btn').style.display = 'block'; } else { console.error('登录失败:', response.error); } } ); } catch (err) { console.error('登录流程异常:', err); } }); // 生成随机字符串作为PKCE验证器 function generateRandomString(length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; const array = new Uint8Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { result += chars[array[i] % chars.length]; } return result; } // 生成PKCE挑战码 async function generatePKCEChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); }
3. 后台脚本交换令牌(Service Worker)
在后台Service Worker中,接收授权码并向SSO提供商请求访问令牌:
// background.js chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'exchangeToken') { const manifest = chrome.runtime.getManifest(); const redirectUrl = chrome.identity.getRedirectURL(); // 构造令牌请求参数 const formData = new URLSearchParams({ client_id: manifest.oauth2.client_id, code: request.code, grant_type: 'authorization_code', redirect_uri: redirectUrl, code_verifier: request.codeVerifier }); // 发送令牌请求 fetch(manifest.oauth2.token_endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData }) .then(res => res.json()) .then(data => { if (data.access_token) { sendResponse({ success: true, token: data.access_token }); } else { sendResponse({ success: false, error: data.error_description || '令牌交换失败' }); } }) .catch(err => { sendResponse({ success: false, error: err.message }); }); // 标记为异步响应 return true; } });
4. 验证登录状态与令牌管理
在扩展启动时,从chrome.storage.local读取令牌并验证有效性,处理过期或无效的情况:
// popup.js document.addEventListener('DOMContentLoaded', async () => { const { accessToken } = await chrome.storage.local.get('accessToken'); if (accessToken) { const isValid = await validateToken(accessToken); if (isValid) { document.getElementById('login-btn').style.display = 'none'; document.getElementById('logout-btn').style.display = 'block'; } else { chrome.storage.local.remove('accessToken'); } } }); // 验证令牌有效性(以Google为例) async function validateToken(token) { const res = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', { headers: { Authorization: `Bearer ${token}` } }); return res.ok; } // 登出逻辑 document.getElementById('logout-btn').addEventListener('click', () => { chrome.storage.local.remove('accessToken'); document.getElementById('login-btn').style.display = 'block'; document.getElementById('logout-btn').style.display = 'none'; });
关键注意事项
- PKCE强制使用:MV3中禁止隐式OAuth流程,必须使用授权码流程+PKCE,防止授权码被拦截。
- 权限配置:确保
host_permissions包含SSO提供商的授权、令牌及验证端点域名,否则请求会被拦截。 - CSP规则:MV3默认CSP不允许
unsafe-inline,所有脚本需放在外部文件中,避免违反规则。 - 令牌存储:使用
chrome.storage.local持久化存储令牌,不要存储在内存中,同时注意令牌过期时间,及时实现刷新逻辑。
内容的提问来源于stack exchange,提问作者Siddhartha Choubey
相关产品推荐
相关产品推荐

