前端跨域状态共享的实现方案

跨域状态共享的实现方案

背景

项目需要实现不同网站(但是顶级域名相同)之间的状态共享,例如登录状态、用户信息。

实现方式

通过 Cookie 共享实现状态同步,在一个网站登录之后,将登录信息及用户信息保存到 Cookie 中,跳转到其他同顶级域名的网站可以从 Cookie 中获取登录及用户信息,不需要再次登录。

方案概述

在实际开发中,我们经常会遇到需要在多个域名之间共享用户登录状态的需求。本文将介绍两种常见的跨域状态共享实现方案,分别适用于不同的场景。

场景一:三个域名是同一顶级域名的子域名

场景描述

假设有三个子域名:

  • a.example.com
  • b.example.com
  • c.example.com

这三个域名都属于 example.com 这个顶级域名。

解决方案:Cookie 共享

由于是同一顶级域名的子域名,可以通过 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':设置为顶级域名,允许所有子域名共享该 Cookie
  • httpOnly: true:防止 XSS 攻击,JavaScript 无法通过 document.cookie 访问
  • secure: true:仅在 HTTPS 连接下传输,提高安全性

各个网站的导航栏组件读取共享的 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.com
  • b.net
  • c.org

这些域名之间无法直接共享 Cookie。

解决方案一:单点登录(SSO)(企业首选)

方案描述

搭建统一的 SSO 服务器,用户登录一个网站后,其他网站自动同步登录状态。

核心流程
  1. 用户访问网站 A:如果未登录,重定向到 SSO 服务器的登录页面
  2. 用户登录 SSO:SSO 生成 JWT token,存储到 Redis,然后重定向回网站 A,通过 URL 参数或 Cookie 传递 token
  3. 网站 A 存储 token:将 token 存储到 LocalStorage,更新导航栏的登录状态
  4. 用户访问网站 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 任意域名 不需要服务器,纯前端实现 需要公共域名,安全性相对较低

总结

选择合适的跨域状态共享方案需要根据实际场景:

  1. 如果是同一顶级域名的子域名:优先使用 Cookie 共享方案,简单高效
  2. 如果是完全不同的顶级域名
    • 企业级应用:推荐使用 SSO 方案,安全性更高
    • 小型项目或快速原型:可以考虑 iframe + postMessage 方案

无论选择哪种方案,都需要注意安全性,包括:

  • 防止 XSS 攻击(使用 httpOnly Cookie)
  • 验证消息来源(检查 origin
  • 使用 HTTPS 传输敏感数据
  • 设置合理的 token 过期时间

希望本文能帮助你选择合适的跨域状态共享方案!

相关推荐
bkspiderx1 天前
用Nginx解决HTTP跨域问题:两种实用方案详解
nginx·http·跨域·http跨域
源代码•宸1 天前
goframe框架签到系统项目开发(用户认证中间件、实现Refresh-token接口)
数据库·经验分享·后端·算法·中间件·跨域·refreshtoken
清羽_ls9 天前
Iframe嵌套网页
iframe·跨域
这是个栗子11 天前
【前端知识点总结】前端跨域问题
前端·跨域·cors
闲人编程12 天前
CORS跨域配置与安全策略
中间件·origin·跨域·cors·codecapsule·分离配置·最小权限
随风一样自由20 天前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案详解
前端·react.js·前端框架·跨域
漂流幻境1 个月前
Spring cloud gateway 跨域配置与碰到的问题
java·gateway·springcloud·跨域
linweidong2 个月前
VIVO前端面试题及参考答案
前端·跨域·localstorage·重绘·浏览器兼容·git管理·前端重构
尽兴-2 个月前
[特殊字符] 微前端部署实战:Nginx 配置 HTTPS 与 CORS 跨域解决方案(示例版)
前端·nginx·https·跨域·cors·chrom