跨域SSO认证系统安全传参方案咨询(Node.js+React)
针对你遇到的URL传Token留痕的安全问题,结合Node.js+Express和React技术栈,给你两个实用的解决方案:
1. 基于PostMessage+隐藏iframe的前端安全传递
这个方案完全规避了URL传参的风险,Token仅在前端页面间通过内存传递,不会被浏览器历史、服务器日志记录。
实现步骤:
- 认证域(auth.aettr.pt):用户登录成功后,创建隐藏iframe指向应用域的专门接收页面,通过
postMessage把Token发送给iframe - 应用域(my.aettr.pt):接收页面监听
message事件,验证消息来源后存储Token,再通知认证域关闭iframe并跳转至应用主页面
认证域React代码示例:
// 登录成功后的处理逻辑 const handleLoginSuccess = (token) => { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = 'https://my.aettr.pt/sso-receiver'; iframe.onload = () => { // 严格指定目标域名,防止恶意页面拦截 iframe.contentWindow.postMessage( { type: 'SSO_AUTH_TOKEN', token }, 'https://my.aettr.pt' ); }; // 监听应用域的接收确认 window.addEventListener('message', (event) => { if (event.origin === 'https://my.aettr.pt' && event.data.type === 'TOKEN_RECEIVED') { document.body.removeChild(iframe); // 跳转到应用主页 window.location.href = 'https://my.aettr.pt/dashboard'; } }, { once: true }); document.body.appendChild(iframe); };
应用域接收页面(React组件):
// my.aettr.pt/sso-receiver import { useEffect } from 'react'; const SSOReceiver = () => { useEffect(() => { const handleMessage = (event) => { // 必须验证来源,防止跨域攻击 if (event.origin !== 'https://auth.aettr.pt') return; if (event.data.type === 'SSO_AUTH_TOKEN' && event.data.token) { // 用sessionStorage存储Token,仅当前会话有效 sessionStorage.setItem('auth_token', event.data.token); // 通知认证域已接收完成 event.source.postMessage({ type: 'TOKEN_RECEIVED' }, event.origin); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []); return <div style={{ display: 'none' }}></div>; }; export default SSOReceiver;
应用域Express配置:
需要允许认证域嵌入接收页面,设置CSP头:
app.use((req, res, next) => { if (req.path === '/sso-receiver') { // 只允许auth.aettr.pt嵌入这个页面 res.setHeader('Content-Security-Policy', 'frame-ancestors https://auth.aettr.pt'); } next(); });
2. 加密URL片段(Fragment)传递
如果觉得iframe方案稍复杂,可以用URL片段(#后面的内容)传递——片段不会被发送到服务器,也不会出现在后端日志里,仅存在浏览器本地历史。配合加密进一步降低风险。
实现步骤:
- 认证域用对称加密(比如AES)加密Token,把加密后的内容放到URL片段里重定向到应用域
- 应用域前端读取片段内容,解密得到Token后,用
history.replaceState清空片段,避免留在地址栏
认证域Node.js加密代码:
const crypto = require('crypto'); // 预先约定的AES密钥(32字节,对应AES-256)和IV长度 const ENCRYPTION_KEY = Buffer.from('your_32_byte_secure_secret_key', 'utf8'); const IV_LENGTH = 16; function encryptToken(token) { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); let encrypted = cipher.update(token); encrypted = Buffer.concat([encrypted, cipher.final()]); // 返回IV+加密内容,用冒号分隔 return `${iv.toString('hex')}:${encrypted.toString('hex')}`; } // 登录成功后的重定向逻辑 app.get('/auth-success', (req, res) => { const authToken = 'your_generated_jwt_or_token'; const encryptedToken = encryptToken(authToken); res.redirect(`https://my.aettr.pt/#encrypted_token=${encryptedToken}`); });
应用域React解密代码:
import { useEffect } from 'react'; const crypto = require('crypto'); const ENCRYPTION_KEY = Buffer.from('your_32_byte_secure_secret_key', 'utf8'); function decryptToken(encryptedText) { const [ivHex, encryptedHex] = encryptedText.split(':'); const iv = Buffer.from(ivHex, 'hex'); const encryptedBuffer = Buffer.from(encryptedHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); let decrypted = decipher.update(encryptedBuffer); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); } const App = () => { useEffect(() => { const hashParams = new URLSearchParams(window.location.hash.slice(1)); const encryptedToken = hashParams.get('encrypted_token'); if (encryptedToken) { try { const token = decryptToken(encryptedToken); sessionStorage.setItem('auth_token', token); // 清除地址栏的片段,避免Token泄露 history.replaceState({}, document.title, window.location.pathname); } catch (err) { console.error('Token解密失败', err); // 解密失败则跳回认证页 window.location.href = 'https://auth.aettr.pt/login'; } } }, []); return <div>应用主内容</div>; }; export default App;
方案对比
- PostMessage+iframe方案:安全性最高,Token完全不经过URL,适合对安全要求高的场景
- 加密URL片段方案:实现更简单,适合快速迭代的场景,但浏览器本地历史仍会保留加密后的内容,风险略高
内容的提问来源于stack exchange,提问作者Tomás Figueiredo
相关产品推荐
相关产品推荐

