使用Google OAuth弹窗时window.opener为null?如何传递访问令牌?
问题根源
弹窗从后端OAuth初始化端点跳转到Google登录页时,跨域操作触发浏览器安全机制,导致window.opener被重置为null,无法直接通过postMessage传递令牌。
方案1:前端同源页面中转初始化流程(最可靠)
通过前端同源页面作为弹窗入口,避免直接打开跨域的Google授权页,确保window.opener始终有效:
父窗口打开同源弹窗
// 父窗口代码:打开前端空白中转页面 const authPopup = window.open('/auth/google/init', 'GoogleAuth', 'width=600,height=600');中转页面请求后端OAuth初始化接口
在/auth/google/init页面中,请求后端获取Google授权URL,再跳转至授权页:// 前端中转页面代码 async function initAuthFlow() { try { // 请求后端生成Google授权地址 const res = await fetch('/api/auth/google/start'); const { authUrl } = await res.json(); // 跳转到Google登录页(此时opener仍指向父窗口) window.location.href = authUrl; } catch (err) { console.error('Auth init failed:', err); window.close(); } } initAuthFlow();后端回调重定向至前端处理页
后端Passport回调逻辑生成JWT后,重定向到前端的令牌处理页面(如/auth/google/callback),并在URL参数中携带令牌:// 后端回调路由示例(Node.js + Passport) app.get('/api/auth/google/callback', passport.authenticate('google', { session: false }), (req, res) => { const token = generateJwtToken(req.user); // 重定向到前端处理页,携带令牌 res.redirect(`/auth/google/callback?token=${token}`); } );前端处理页发送令牌至父窗口
在/auth/google/callback页面中,提取令牌并通过postMessage发送给父窗口,随后关闭弹窗:// 前端处理页代码 function handleAuthCallback() { const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); if (token && window.opener) { // 发送令牌到父窗口,限制同源接收 window.opener.postMessage( { type: 'GOOGLE_AUTH_SUCCESS', token }, window.opener.origin ); } window.close(); } handleAuthCallback();
方案2:临时Code换令牌(避免URL暴露JWT)
若担心JWT在URL中暴露,可通过后端临时存储中转:
后端回调生成临时Code
后端生成JWT后,将其存入Redis等临时存储(设置短过期时间,如5分钟),并返回临时Code给前端:// 后端回调路由示例 const redis = require('redis'); const client = redis.createClient(); const { v4: uuidv4 } = require('uuid'); app.get('/api/auth/google/callback', passport.authenticate('google', { session: false }), async (req, res) => { const token = generateJwtToken(req.user); const tempCode = uuidv4(); // 生成唯一临时Code await client.setEx(tempCode, 300, token); // 存储5分钟 res.redirect(`/auth/google/callback?code=${tempCode}`); } );前端用Code换JWT
前端处理页通过临时Code请求后端换取JWT,再发送给父窗口:async function exchangeToken() { const urlParams = new URLSearchParams(window.location.search); const tempCode = urlParams.get('code'); if (tempCode && window.opener) { const res = await fetch(`/api/auth/google/exchange?code=${tempCode}`); const { token } = await res.json(); window.opener.postMessage( { type: 'GOOGLE_AUTH_SUCCESS', token }, window.opener.origin ); } window.close(); } exchangeToken();后端提供换令牌接口
app.get('/api/auth/google/exchange', async (req, res) => { const { code } = req.query; const token = await client.get(code); if (token) { await client.del(code); // 用完即删 res.json({ token }); } else { res.status(400).json({ error: 'Invalid or expired code' }); } });
方案3:Broadcast Channel API(无依赖opener)
利用浏览器的Broadcast Channel API,实现同源窗口间的消息传递,无需依赖window.opener:
父窗口监听广播
// 父窗口代码 const authChannel = new BroadcastChannel('google-auth-channel'); authChannel.onmessage = (event) => { if (event.data.type === 'GOOGLE_AUTH_SUCCESS') { // 处理令牌逻辑 console.log('Received token:', event.data.token); authChannel.close(); } }; // 打开弹窗(直接指向后端初始化端点也可) window.open('/api/auth/google', 'GoogleAuth', 'width=600,height=600');弹窗发送广播
在前端回调页面中,提取令牌并发送广播:// 前端回调页代码 const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); if (token) { const authChannel = new BroadcastChannel('google-auth-channel'); authChannel.postMessage({ type: 'GOOGLE_AUTH_SUCCESS', token }); authChannel.close(); window.close(); }注意:该方案要求浏览器支持Broadcast Channel API,且窗口必须同源。
内容的提问来源于stack exchange,提问作者Cytronic

