跨域状态共享的实现方案
背景
项目需要实现不同网站(但是顶级域名相同)之间的状态共享,例如登录状态、用户信息。
实现方式
通过 Cookie 共享实现状态同步,在一个网站登录之后,将登录信息及用户信息保存到 Cookie 中,跳转到其他同顶级域名的网站可以从 Cookie 中获取登录及用户信息,不需要再次登录。
方案概述
在实际开发中,我们经常会遇到需要在多个域名之间共享用户登录状态的需求。本文将介绍两种常见的跨域状态共享实现方案,分别适用于不同的场景。
场景一:三个域名是同一顶级域名的子域名
场景描述
假设有三个子域名:
a.example.comb.example.comc.example.com
这三个域名都属于 example.com 这个顶级域名。
解决方案:Cookie 共享
由于是同一顶级域名的子域名,可以通过 Cookie 共享 快速实现状态同步。
1. 设置跨域 Cookie
当登录接口返回 Cookie 时,将 Domain 属性设置为顶级域名(如 .example.com),Path 设置为 /,这样所有子域名都可以访问到这个 Cookie。
代码示例(Node.js/Express):
javascript
// 登录成功后设置 Cookie
res.cookie('token', userToken, {
domain: '.example.com', // 设置为顶级域名,所有子域名都可以访问
path: '/', // 路径设置为根路径
httpOnly: true, // 防止 XSS 攻击,JavaScript 无法访问
secure: true, // 仅在 HTTPS 下传输
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 天过期
});
说明:
domain: '.example.com':设置为顶级域名,允许所有子域名共享该 CookiehttpOnly: true:防止 XSS 攻击,JavaScript 无法通过document.cookie访问secure: true:仅在 HTTPS 连接下传输,提高安全性
2. 导航栏读取 Cookie
各个网站的导航栏组件读取共享的 token,然后向服务器发送请求验证登录状态。
代码示例(Web Components):
javascript
// 读取共享的 token
function getToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'token') {
return value;
}
}
return null;
}
// 验证登录状态
async function checkLoginStatus() {
const token = getToken();
if (!token) {
return false;
}
try {
const response = await fetch('https://api.example.com/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token })
});
const data = await response.json();
return data.isValid;
} catch (error) {
console.error('验证登录状态失败:', error);
return false;
}
}
场景二:三个域名是完全不同的顶级域名
场景描述
假设有三个完全不同的顶级域名:
a.comb.netc.org
这些域名之间无法直接共享 Cookie。
解决方案一:单点登录(SSO)(企业首选)
方案描述
搭建统一的 SSO 服务器,用户登录一个网站后,其他网站自动同步登录状态。
核心流程
- 用户访问网站 A:如果未登录,重定向到 SSO 服务器的登录页面
- 用户登录 SSO:SSO 生成 JWT token,存储到 Redis,然后重定向回网站 A,通过 URL 参数或 Cookie 传递 token
- 网站 A 存储 token:将 token 存储到 LocalStorage,更新导航栏的登录状态
- 用户访问网站 B:如果未登录,重定向到 SSO。SSO 检测到用户已登录,生成 token,重定向回网站 B,实现自动登录
技术实现
SSO 服务器:
- 可以基于 OAuth 2.0/OpenID Connect 协议(如 Keycloak、Auth0)
导航栏验证:
- Web Components 使用
fetch调用 SSO token 验证 API 获取用户信息
代码示例(JavaScript):
javascript
// 获取用户信息
async function getUserInfo() {
// 从 LocalStorage 获取 SSO token
const ssoToken = localStorage.getItem('sso_token');
if (!ssoToken) {
return null;
}
try {
// 调用 SSO 验证接口
const response = await fetch('https://sso.example.com/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: ssoToken })
});
const data = await response.json();
if (data.isValid) {
return data.user;
} else {
// token 无效,清除本地存储
localStorage.removeItem('sso_token');
return null;
}
} catch (error) {
console.error('获取用户信息失败:', error);
return null;
}
}
// 使用示例
async function initNavbar() {
const userInfo = await getUserInfo();
if (userInfo) {
// 更新导航栏显示已登录状态
updateNavbar(userInfo);
} else {
// 显示登录按钮
showLoginButton();
}
}
解决方案二:iframe + postMessage 跨域通信
方案描述
使用一个公共域名的 iframe 作为中间件,三个网站通过 postMessage 与这个 iframe 通信,共享登录状态。
实现步骤
1. 部署中间件 iframe
在公共域名(如 storage.example.com)部署一个页面,用于存储登录状态(如 LocalStorage)。
2. 网站嵌入 iframe
所有三个网站都嵌入这个 iframe(隐藏),使用 postMessage 向 iframe 发送/接收登录状态。
代码示例(网站 A - 发送状态):
html
<!-- 在 HTML 中嵌入隐藏的 iframe -->
<iframe
id="storage-iframe"
src="https://storage.example.com/storage.html"
style="display: none;"
></iframe>
<script>
// 发送登录状态到 iframe
function setUserStatus(userData) {
const iframe = document.getElementById('storage-iframe');
iframe.onload = function() {
// 向 iframe 发送消息
iframe.contentWindow.postMessage({
type: 'SET_USER',
data: {
name: userData.name,
token: userData.token
}
}, 'https://storage.example.com');
};
}
// 使用示例:用户登录后
setUserStatus({
name: 'John Doe',
token: 'abc123token'
});
</script>
代码示例(网站 B - 接收状态):
html
<!-- 在 HTML 中嵌入隐藏的 iframe -->
<iframe
id="storage-iframe"
src="https://storage.example.com/storage.html"
style="display: none;"
></iframe>
<script>
// 监听来自 iframe 的消息
window.addEventListener('message', function(e) {
// 验证消息来源,防止跨站攻击
if (e.origin !== 'https://storage.example.com') {
return;
}
// 处理消息
if (e.data.type === 'GET_USER_RESPONSE') {
const userData = e.data.data;
if (userData) {
console.log('用户信息:', userData);
// 更新导航栏显示已登录状态
updateNavbar(userData);
} else {
// 显示登录按钮
showLoginButton();
}
}
});
// 向 iframe 请求用户信息
function getUserStatus() {
const iframe = document.getElementById('storage-iframe');
iframe.onload = function() {
// 发送获取用户信息的请求
iframe.contentWindow.postMessage({
type: 'GET_USER'
}, 'https://storage.example.com');
};
}
// 页面加载时获取用户状态
window.addEventListener('DOMContentLoaded', getUserStatus);
</script>
3. iframe 处理消息
iframe 监听 message 事件,读取/写入登录状态到自己的 LocalStorage,并将数据发送回源窗口。
代码示例(iframe 页面 - storage.example.com/storage.html):
html
<!DOCTYPE html>
<html>
<head>
<title>Storage Iframe</title>
</head>
<body>
<script>
// 允许的域名白名单
const ALLOWED_ORIGINS = [
'https://a.com',
'https://b.net',
'https://c.org'
];
// 监听来自父窗口的消息
window.addEventListener('message', function(e) {
// 验证消息来源
if (!ALLOWED_ORIGINS.includes(e.origin)) {
console.warn('拒绝来自未授权域名的消息:', e.origin);
return;
}
const { type, data } = e.data;
switch (type) {
case 'SET_USER':
// 存储用户信息到 LocalStorage
localStorage.setItem('user_data', JSON.stringify(data));
console.log('用户信息已存储:', data);
break;
case 'GET_USER':
// 从 LocalStorage 读取用户信息
const userData = localStorage.getItem('user_data');
const parsedData = userData ? JSON.parse(userData) : null;
// 发送用户信息回父窗口
e.source.postMessage({
type: 'GET_USER_RESPONSE',
data: parsedData
}, e.origin);
break;
case 'CLEAR_USER':
// 清除用户信息
localStorage.removeItem('user_data');
console.log('用户信息已清除');
break;
default:
console.warn('未知的消息类型:', type);
}
});
console.log('Storage iframe 已加载');
</script>
</body>
</html>
方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Cookie 共享 | 同一顶级域名的子域名 | 实现简单,性能好 | 仅适用于子域名场景 |
| SSO | 任意域名 | 安全性高,企业级方案 | 需要搭建 SSO 服务器,实现复杂 |
| iframe + postMessage | 任意域名 | 不需要服务器,纯前端实现 | 需要公共域名,安全性相对较低 |
总结
选择合适的跨域状态共享方案需要根据实际场景:
- 如果是同一顶级域名的子域名:优先使用 Cookie 共享方案,简单高效
- 如果是完全不同的顶级域名 :
- 企业级应用:推荐使用 SSO 方案,安全性更高
- 小型项目或快速原型:可以考虑 iframe + postMessage 方案
无论选择哪种方案,都需要注意安全性,包括:
- 防止 XSS 攻击(使用
httpOnlyCookie) - 验证消息来源(检查
origin) - 使用 HTTPS 传输敏感数据
- 设置合理的 token 过期时间
希望本文能帮助你选择合适的跨域状态共享方案!