iframe页面嵌入登陆后的首页

1,073 阅读6分钟

1.问题场景

A系统开发了一个新首页,B系统的领导看到了页面觉得很满意,可是又不想花费成本进行开发,就让技术人员想想办法; 技术人员就想:这还不简单,使用iframe嵌入不就ok了;

2.A系统情况

首先A系统有一个登陆页面,在登陆过后可以获取后台返回的token,将token存储在sessionStorage中或者cookie中,然后在后续发起的请求中将token放在请求头中,请求数据接口; 由此A系统的情况分析如下:

  1. A系统对接口有需要携带token的鉴权接口和不需要写到token的白名单接口(白名单接口可以在后端的配置文件中进行配置)
  2. 首页展示数据请求的接口是需要鉴权的接口(需要登陆之后才能看到相应的页面数据,还是比较敏感的,不能做白名单)
  3. 那么我们必须走一遍登陆流程,模仿正常的登陆

3.解决思路

  1. 首先需要保证A系统和B系统的网络是互通的:在A系统的服务器中pingB系统的ip
ping [服务器ip]
  1. A系统在iframe嵌入之前需要获取登陆的token,直接调用B系统登陆接口,传入B系统的用户名和密码
<iframe :src = 'iframeUrl' width='100%' height='100%'>
const iframeUrl =ref(null)
const getToken=async()=>{
    let token = null
    const param={
        userName:'A',
        password:'123456',
    }
    await getBSystemTOken(param).then(res=>{
        token = res.data.token
    })
    iframeUrl.value = 'http://...' // B系统的登陆页面url
    if(token){
        window.postMessage(token, iframeUrl.value);
    }else{
        ElMessage.warn('获取toekn失败')
    }
}
  1. B系统在原本的页面逻辑里面,在login.vue组件的生命周期created中原本只有获取验证码的接口和对接口的传参是否加密,现需要增加对message的监听,当监听到来自A系统的通知时,需要将store中的全局变量存储传过来的token (1)准备在仓库中存储的字段 store/module/loginStore.js
import { defineStore} from 'pinia'
import { ref} from 'vue'

export const usrLoginStore  = definsStore('login',()=>{
     // 存储第三方系统访问来的token
    const thirdSystemToken = ref(null)
    return {
        thirdSystemToken
    }

})

store/index.vue

import {createPinia} from 'pinia'
import {useLoginStore} from '/@/store/module/loginStore.js'

const pinia = createPinia()
export default pinia
export {
    useLoginStore
}

(2)在login.vue页面中监听message并对全局变量赋值 login.vue

<script setup>
import {storeToRefs} from 'pinia'
import {useLoginStore} from '/@/store/index.vue'

const storeLogin = storeToRefs(useLoginStore)
...
onMounted(()=>{
    window.addEventListener('message',(event)=>{
        if(event.origin == 'http://...'){//A系统的url
            if(event.data.data){ // 传过来的必须有参数
                storeLogin.thirdSystemToken.value = event.data.data
                // 跳转到首页,触发路由跳转
                router.push({
                    path:'/workBanch'
                })
            }
        } 
        
    })
})
</setup>

(3)在vue Router的路由拦截器中判断全局变量是否有值,有值的话说明来自第三方系统,对路由放行,没有值的话走正常路由跳转流程 router/index.vue

import Cookies from 'js-cookie'
import {createRouter,createWebHistory} from 'vue-router'
import {storeToRefs} from 'pinia'
import {useLoginStore} from '/@/store/index.vue'

const storeLogin = storeToRefs(useLoginStore)

const router = createRouter({
    history:createWebHistory(),
    routers:[],
})

const getToken=()=>{
    return sessionStorage.getItem('token') || Cookies.get('token')
}

router.beforeEach((to,from,next)=>{
    const thirdSysToken = storeLogin.thirdSystemToken.value
    if(thirdSysToken){// 如果有值直接放行
        next();
        NProgress.done();
    }else if(getToken()){
        // 其余路由逻辑
        ...
    }
})

(4)在axios的请求拦截中对请求添加header:Authorization

import axios from 'axios'
import {storeToRefs} from 'pinia'
import {useLoginStore} from '/@/store/index.vue'

const storeLogin = storeToRefs(useLoginStore)

// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 30000
})

// request拦截器
service.interceptors.request.use(config => {
  ...
  
  const thirdPartSysToken = sstoreLogin.thirdSystemToken.value
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if ((getToken() && !isToken) || thirdPartSysToken) {
    config.headers['Authorization'] = 'bearer ' + (thirdPartSystem ? thirdPartSysToken : getToken()) // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  return config
}, error => {
  Promise.reject(error)
})

4.iframe页面无法操作cookie

我们经常会听见这句话:“iframe页面无法操作cookie” 仔细解读一下:iframe页面是嵌入到浏览器的一个子页面,它有一个父页面,父页面处在浏览器环境中,父页面的js代码可以操作浏览器的cookie和session,子页面无法操作父页面所在浏览器的cookie和session. 究其原因:iframe的window对象与父页面的window对象是不同的,但在同源情况下,它们可以相互访问和通信,所以,在不同源的系统中,iframe页面无法操作父页面的window对象. 如果我们想操作父页面window对象,只有一个办法,和父页面进行通信,让父页面操作window对象 iframe页面

const param = {
    msg:'我想操作父页面的cookie'
}
window.parent.postMessage(params, "http://..."); // 父页面的url

父页面

window.addEventListener('message', function(event) { 
    // 确保消息来自可信源 
    if (event.origin !== 'http://example.com') { 
        return; 
        // 忽略不可信的消息 
    } 
    console.log('接收到来自 iframe 的消息:', event.data); 
    // Todo 操作window对象
});

5.iframe嵌入后报拒绝连接请求

通常是目标页面设置了X-Frame-Options响应头来限制内容被嵌入到其他站点的iframe中

image.png 这个配置通常是在nginx中进行配置的

server { 
    listen 80; # 监听 80 端口 
    server_name your_domain.com; # 替换为您的域名 
    # 设置 X-Frame-Options 响应头 
    add_header X-Frame-Options "DENY"; # 或者使用 SAMEORIGIN 
    
    location / { r
        oot /var/www/html; 
        # 网站根目录 index index.html index.htm; 
        try_files $uri $uri/ =404; # 处理请求 
    } 
    # 其他 location 块可以根据需要添加 
}

X-Frame-Options可以配置的值有:DENY、SAMEORIGIN、ALLOW-FROM (1)DENY:完全禁止页面被嵌入到iframe中 (2)SAMEORIGIN:允许同源页面嵌入该页面,即只有与该页面同源的页面可以使用iframe嵌入 (3)ALLOW-FROM:已废弃,2015年后谷歌浏览器较新版不支持ALLOW-FROM,作为替代,我们可以使用Content-Security-Policy

6.Content-Security-Policy允许特定url嵌入iframe

add_header Content-Security-Policy "frame-ancestors 'self' http://...";

X-Frame-Options和Content-Security-Policy(CSP)的frame-ancestors指令可以在同一响应中共存,但它们之间有一些重要的差异和优先级 (1)共存性 可以在同一响应中同时设置X-Frame-Options和Content-Security-Policy

add_header X-Frame-Options "DENY";  # 或 "SAMEORIGIN"
add_header Content-Security-Policy "frame-ancestors 'self'";

(2)优先级 CSP优先: 如果在响应中同时存在X-Frame-Options和Content-Security-Policy,则大多数现代浏览器会优先考虑Content-Security-Policy的frame-ancestors指令。这意味着即使X-Frame-Options设置为 DENY,如果 CSP 允许某些源,浏览器将遵循 CSP 的指令 (3)建议的做法 使用 CSP: 由于Content-Security-Policy提供了更细粒度的控制,建议使用 CSP 的frame-ancestors指令来替代X-Frame-Options. 保留 X-Frame-Options: 如果您需要向旧版浏览器提供支持,仍然可以保留X-Frame-Options,以确保在不支持 CSP 的浏览器中也能提供一定的保护 示例

server {
    listen 80;
    server_name your_domain.com;

    # X-Frame-Options
    add_header X-Frame-Options "DENY";

    # Content Security Policy
    add_header Content-Security-Policy "frame-ancestors 'self'";

    location / {
        root /var/www/html;
        index index.html index.htm;
    }
}