在实际开发中,我们经常需要将一个系统嵌入到另一个系统中,但如何实现免登录呢?本文将为你详细讲解两种方案,并告诉你为什么推荐使用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接收成功,已实现免登录')
}
}
}
为什么我不推荐这个方案?
- 安全风险极高:token明文暴露在URL中,容易被截获
- 浏览器历史记录:URL会保存在浏览器历史中,任何人都能看到
- 日志泄露:服务器日志、Referer头都可能记录包含token的URL
- 分享风险:用户不小心分享了这个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已过期,拒绝接收')
}
}
}
}
}
为什么强烈推荐这个方案?
- 安全性高:token不会暴露在URL中
- 灵活性强:可以传递复杂对象,支持双向通信
- 可控性强:可以验证消息来源(origin),防止XSS攻击
- 时效性控制:可以添加时间戳,防止重放攻击
三、重要注意事项
监听必须放在App.vue的created中!
这是很多开发者容易踩的坑。为什么不能放在被嵌入的具体子页面组件中?
因为Vue的初始化流程:
- App.vue先创建(created)
- 然后才是路由匹配,加载子组件
- 而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冲突
最后提醒
- 开发环境:可以先用URL传参快速验证功能,但上线前一定要改成postMessage
- 调试技巧:在控制台打印消息收发日志,方便排查问题
- 错误处理:做好异常处理,当token失效时优雅降级到登录页