Supabase OAuth登录回调URL含#而非?,服务端无法获取参数
解决Express + Supabase Auth谷歌登录回调参数在哈希中的问题
问题原因
你遇到的哈希(#)参数问题,是因为Supabase的signInWithOAuth默认是为前端单页应用(SPA)设计的流程:谷歌授权后会先回调到Supabase的OAuth端点,Supabase再将token通过URL哈希(而非查询参数)重定向到你指定的redirectTo地址——哈希参数只会在浏览器前端解析,服务端无法直接获取。
解决方案
方案一:前端中转哈希参数到服务端
在回调路由返回一个极简的HTML页面,通过前端JS提取哈希参数,再重定向到服务端的处理路由(将参数转为查询参数):
// 回调路由:返回前端中转页面 app.get('/login/google/callback', (req, res) => { res.send(` <!DOCTYPE html> <html> <body> <script> // 提取哈希中的参数 const hashParams = new URLSearchParams(window.location.hash.slice(1)); const queryString = hashParams.toString(); // 重定向到服务端处理路由 window.location.href = '/login/google/callback/handle?' + queryString; </script> </body> </html> `); }); // 处理参数的服务端路由 app.get('/login/google/callback/handle', async (req, res) => { const { access_token, refresh_token } = req.query; // 验证token并获取用户信息 const { data: { user }, error } = await supabase.auth.getUser(access_token); if (error) { return res.status(401).send('无效的登录凭证'); } // 这里添加你的会话设置逻辑(比如用express-session) req.session.user = user; req.session.accessToken = access_token; req.session.refreshToken = refresh_token; // 跳转至用户主页 res.redirect('/dashboard'); });
方案二:直接使用谷歌授权码流(服务端原生处理)
绕过Supabase的前端导向流程,手动构建谷歌OAuth授权URL,让谷歌直接回调到你的Express路由,再通过授权码交换token并同步到Supabase:
步骤1:配置谷歌OAuth客户端
在谷歌云平台的OAuth 2.0客户端设置中,将授权重定向URI设为http://localhost:3000/login/google/callback。
步骤2:构建谷歌授权URL
const querystring = require('querystring'); app.get('/login/google', (req, res) => { // 生成随机state值防止CSRF攻击 const state = Math.random().toString(36).substring(2, 15); req.session.oauthState = state; const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + querystring.stringify({ client_id: '你的谷歌客户端ID', redirect_uri: 'http://localhost:3000/login/google/callback', response_type: 'code', scope: 'email profile', access_type: 'offline', // 必须添加才能获取refresh_token state: state }); res.redirect(authUrl); });
步骤3:处理回调并交换token
const axios = require('axios'); app.get('/login/google/callback', async (req, res) => { const { code, state } = req.query; // 验证state防止CSRF if (state !== req.session.oauthState) { return res.status(403).send('非法请求'); } // 用授权码交换谷歌token try { const tokenResponse = await axios.post( 'https://oauth2.googleapis.com/token', querystring.stringify({ code: code, client_id: '你的谷歌客户端ID', client_secret: '你的谷歌客户端密钥', redirect_uri: 'http://localhost:3000/login/google/callback', grant_type: 'authorization_code' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); const { id_token, access_token, refresh_token } = tokenResponse.data; // 用谷歌id_token登录Supabase const { data: { session }, error } = await supabase.auth.signInWithIdToken({ provider: 'google', token: id_token }); if (error) throw error; // 设置会话 req.session.supabaseSession = session; res.redirect('/dashboard'); } catch (err) { res.status(500).send('登录失败:' + err.message); } });
内容的提问来源于stack exchange,提问作者Roberto Valente
相关产品推荐
相关产品推荐

