Vue项目中iframe嵌入页面实现免登录的完整指南

在实际开发中,我们经常需要将一个系统嵌入到另一个系统中,但如何实现免登录呢?本文将为你详细讲解两种方案,并告诉你为什么推荐使用postMessage!

一、问题背景

在企业级应用开发中,我们经常会遇到这样的场景:需要将A系统的某个页面通过iframe嵌入到B系统中。但嵌入后发现,每次打开都会跳转到登录页,因为被嵌入的系统没有登录状态(没有token)。

问题本质:iframe嵌入的页面是一个独立的上下文环境,它无法直接获取父页面的登录凭证。

那么,如何优雅地解决这个问题呢?下面我将为你详细讲解两种方案,并告诉你为什么第二种方案是最佳实践!


二、解决方案对比

方案一:通过URL传参(不推荐)

这是最简单直接的方法,但存在严重的安全隐患。

1. 发送方(主系统)代码
html 复制代码
<template>
  <div>
    <iframe 
      :src="iframeUrl" 
      id="childFrame" 
      name="demo"
      style="width: 100%; height: 800px; border: none;">
    </iframe>
  </div>
</template>

<script>
export default {
  data() {
    return {
      iframeUrl: ''
    }
  },
  mounted() {
    // 获取当前用户的token
    const mytoken = localStorage.getItem('access_token')
    // 拼接URL(注意:这里应该对token进行加密!)
    this.iframeUrl = `http://localhost:8080/dudu?mytoken=${encodeURIComponent(mytoken)}`
  }
}
</script>
2. 接收方(被嵌入系统)代码
javascript 复制代码
// 在 App.vue 的 created 生命周期中
export default {
  created() {
    // 从URL参数中获取token
    const urlParams = new URLSearchParams(window.location.search)
    const token = urlParams.get('mytoken')
    
    if (token) {
      // 将token存入localStorage,实现免登录
      localStorage.setItem('access_token', token)
      console.log('Token接收成功,已实现免登录')
    }
  }
}
为什么我不推荐这个方案?
  1. 安全风险极高:token明文暴露在URL中,容易被截获
  2. 浏览器历史记录:URL会保存在浏览器历史中,任何人都能看到
  3. 日志泄露:服务器日志、Referer头都可能记录包含token的URL
  4. 分享风险:用户不小心分享了这个URL,token就泄露了

如果你必须使用这个方案,请务必对token进行加密处理,并设置短时效性!


方案二:通过postMessage跨窗口通信(推荐)

这是目前最安全、最优雅的解决方案,利用HTML5的postMessageAPI实现跨窗口安全通信。

1. 发送方(主系统)代码
html 复制代码
<template>
  <div>
    <iframe 
      ref="childFrame"
      src="http://localhost:8080/dudu" 
      id="childFrame" 
      name="demo"
      style="width: 100%; height: 800px; border: none;">
    </iframe>
  </div>
</template>

<script>
export default {
  mounted() {
    // 等待iframe加载完成
    this.$refs.childFrame.onload = () => {
      // 构造消息对象
      const params = { 
        type: "setToken", 
        token: localStorage.getItem('access_token'),
        timestamp: Date.now()
      }
      
      // 发送消息给iframe
      // 注意:第二个参数应该指定目标origin,而不是"*",这里为了演示简化
      this.$refs.childFrame.contentWindow.postMessage(params, "*")
      
      console.log('Token已通过postMessage发送')
    }
  }
}
</script>
2. 接收方(被嵌入系统)代码
javascript 复制代码
// 在 App.vue 的 created 生命周期中(非常重要!)
export default {
  created() {
    // 监听message事件
    window.addEventListener("message", this.handleMessage, false)
  },
  beforeDestroy() {
    // 组件销毁时移除监听器,避免内存泄漏
    window.removeEventListener("message", this.handleMessage, false)
  },
  methods: {
    handleMessage(e) {
      // 安全验证:检查消息来源
      // if (e.origin !== 'http://your-main-app.com') return
      
      // 检查消息类型
      if (e.data.type === 'setToken') {
        const { token, timestamp } = e.data
        
        // 验证时效性(10秒内有效,防止重放攻击)
        if (Date.now() - timestamp < 10000) {
          // 将token存入缓存
          localStorage.setItem('access_token', token)
          
          // 可选:刷新当前页面或触发登录状态更新
          this.$store.commit('SET_TOKEN', token)
          this.$store.dispatch('GetUserInfo')
          
          console.log('Token接收成功,已实现免登录')
        } else {
          console.error('Token已过期,拒绝接收')
        }
      }
    }
  }
}
为什么强烈推荐这个方案?
  1. 安全性高:token不会暴露在URL中
  2. 灵活性强:可以传递复杂对象,支持双向通信
  3. 可控性强:可以验证消息来源(origin),防止XSS攻击
  4. 时效性控制:可以添加时间戳,防止重放攻击

三、重要注意事项

监听必须放在App.vue的created中!

这是很多开发者容易踩的坑。为什么不能放在被嵌入的具体子页面组件中?

因为Vue的初始化流程:

  1. App.vue先创建(created)
  2. 然后才是路由匹配,加载子组件
  3. 而postMessage消息可能在子组件加载前就发出了

如果监听器放在子组件中,很可能消息已经发送,但监听器还没注册,导致消息丢失!

javascript 复制代码
//  错误做法:放在子组件中
export default {
  // 这个组件可能还没加载,消息就发出了
  created() {
    window.addEventListener("message", this.handleMessage, false)
  }
}

//  正确做法:放在App.vue中
export default {
  created() {
    window.addEventListener("message", this.handleMessage, false)
  }
}

四、进阶场景处理

场景1:主系统与被嵌入系统不同源

当两个系统不在同一个域名下时,无法直接共享token,需要额外处理:

javascript 复制代码
// 接收方代码
handleMessage(e) {
  if (e.data.type === 'setToken') {
    // 不同源时,不能直接使用对方的token
    // 应该用这个token去调用自己系统的接口换取本系统的token
    this.exchangeToken(e.data.token)
  }
},
async exchangeToken(externalToken) {
  try {
    // 调用本系统的接口,用外部token换取本系统token
    const res = await this.$axios.post('/api/exchange-token', {
      external_token: externalToken
    })
    
    if (res.data.success) {
      // 保存本系统的token
      localStorage.setItem('access_token', res.data.token)
      this.$store.commit('SET_TOKEN', res.data.token)
      console.log('Token交换成功,已实现免登录')
    }
  } catch (error) {
    console.error('Token交换失败', error)
  }
}

场景2:两个系统使用相同的token字段名

如果两个系统都使用access_token作为localStorage的key,可能会造成冲突:

javascript 复制代码
// 推荐做法:使用不同的key
// 主系统:main_access_token
// 被嵌入系统:embedded_access_token

// 在axios请求拦截器中动态选择
axios.interceptors.request.use(config => {
  // 判断当前是否在iframe中
  const isInIframe = window.self !== window.top
  
  if (isInIframe) {
    config.headers['Authorization'] = `Bearer ${localStorage.getItem('embedded_access_token')}`
  } else {
    config.headers['Authorization'] = `Bearer ${localStorage.getItem('main_access_token')}`
  }
  
  return config
})

五、安全加固建议

1. 验证消息来源

javascript 复制代码
handleMessage(e) {
  // 白名单验证
  const allowedOrigins = [
    'http://main-app.com',
    'https://main-app.com'
  ]
  
  if (!allowedOrigins.includes(e.origin)) {
    console.warn('拒绝来自未知源的消息', e.origin)
    return
  }
  
  // 后续处理...
}

2. 添加消息签名

javascript 复制代码
// 发送方
const signature = md5(`${token}${secretKey}${timestamp}`)
const params = { type: "setToken", token, timestamp, signature }

// 接收方
const expectedSignature = md5(`${token}${secretKey}${timestamp}`)
if (signature !== expectedSignature) {
  console.error('签名验证失败')
  return
}

3. 设置token有效期

javascript 复制代码
// 发送方
const params = { 
  type: "setToken", 
  token: localStorage.getItem('access_token'),
  timestamp: Date.now(),
  expire: 300000 // 5分钟有效期
}

// 接收方
if (Date.now() - timestamp > expire) {
  console.error('Token已过期')
  return
}

六、完整示例代码

主系统完整代码

html 复制代码
<template>
  <div class="container">
    <h2>主系统 - 嵌入子系统页面</h2>
    <iframe 
      ref="childFrame"
      :src="iframeSrc" 
      class="iframe-container">
    </iframe>
  </div>
</template>

<script>
export default {
  data() {
    return {
      iframeSrc: 'http://localhost:8080/'
    }
  },
  mounted() {
    this.$refs.childFrame.onload = this.sendToken
  },
  methods: {
    sendToken() {
      const token = localStorage.getItem('access_token')
      
      if (!token) {
        this.$message.error('请先登录主系统')
        return
      }
      
      const params = { 
        type: "setToken", 
        token: token,
        timestamp: Date.now()
      }
      
      // 发送给iframe
      this.$refs.childFrame.contentWindow.postMessage(params, "*")
      
      console.log('Token已发送给子系统')
    }
  }
}
</script>

<style scoped>
.container {
  padding: 20px;
}
.iframe-container {
  width: 100%;
  height: 800px;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin-top: 20px;
}
</style>

被嵌入系统完整代码(App.vue)

javascript 复制代码
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App',
  created() {
    // 注册消息监听器
    window.addEventListener("message", this.handleMessage, false)
    console.log('消息监听器已注册')
  },
  beforeDestroy() {
    // 移除监听器
    window.removeEventListener("message", this.handleMessage, false)
  },
  methods: {
    handleMessage(e) {
      // 安全验证(生产环境应该严格验证origin)
      // if (e.origin !== 'http://main-app.com') return
      
      if (e.data.type === 'setToken') {
        const { token, timestamp } = e.data
        
        // 验证时效性(10秒内有效)
        if (Date.now() - timestamp > 10000) {
          console.error('Token已过期')
          return
        }
        
        // 保存token
        localStorage.setItem('access_token', token)
        
        // 更新Vuex状态(如果使用Vuex)
        if (this.$store) {
          this.$store.commit('SET_TOKEN', token)
          // 可选:获取用户信息
          // this.$store.dispatch('GetUserInfo')
        }
        
        console.log('Token接收成功,已实现免登录')
        
        // 可选:显示成功提示
        if (this.$message) {
          this.$message.success('已自动登录')
        }
      }
    }
  }
}
</script>

七、总结与注意

实施需注意:

  • 监听器放在App.vue的created生命周期中
  • 验证消息来源(origin)
  • 添加时间戳防止重放攻击
  • 设置合理的token有效期
  • 在beforeDestroy中移除监听器
  • 处理不同源场景(token交换)
  • 避免localStorage key冲突

最后提醒

  1. 开发环境:可以先用URL传参快速验证功能,但上线前一定要改成postMessage
  2. 调试技巧:在控制台打印消息收发日志,方便排查问题
  3. 错误处理:做好异常处理,当token失效时优雅降级到登录页
相关推荐
css趣多多2 小时前
Elment UI 布局组件
javascript
无法长大2 小时前
Mac M1 环境下使用 Rust Tauri 将 Vue3 项目打包成 APK 完整指南
android·前端·macos·rust·vue3·tauri·打包apk
LongJ_Sir2 小时前
Cesium--可拖拽气泡弹窗(对话框尾巴,Vue3版)
前端·javascript·vue.js
im_AMBER2 小时前
消失的最后一秒:SSE 流式联调中的“时序竞争”
前端·笔记·学习·http·sse
GDAL2 小时前
Electron IPC 通信深入全面讲解教程
javascript·electron
RFCEO2 小时前
前端编程 课程十、:CSS 系统学习学前知识/准备
前端·css·层叠样式表·动效设计·前端页面布局6大通用法则·黄金分割比例法则·设计美观的前端
白日梦想家6812 小时前
深入浅出 JavaScript 定时器:从基础用法到避坑指南
开发语言·javascript·ecmascript
雄狮少年2 小时前
简单react agent(没有抽象成基类、子类,直接用)--- 非workflow版 ------demo1
前端·react.js·前端框架
一 乐2 小时前
在线考试|基于springboot + vue在线考试系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计