1.问题场景
A系统开发了一个新首页,B系统的领导看到了页面觉得很满意,可是又不想花费成本进行开发,就让技术人员想想办法; 技术人员就想:这还不简单,使用iframe嵌入不就ok了;
2.A系统情况
首先A系统有一个登陆页面,在登陆过后可以获取后台返回的token,将token存储在sessionStorage中或者cookie中,然后在后续发起的请求中将token放在请求头中,请求数据接口; 由此A系统的情况分析如下:
- A系统对接口有需要携带token的鉴权接口和不需要写到token的白名单接口(白名单接口可以在后端的配置文件中进行配置)
- 首页展示数据请求的接口是需要鉴权的接口(需要登陆之后才能看到相应的页面数据,还是比较敏感的,不能做白名单)
- 那么我们必须走一遍登陆流程,模仿正常的登陆
3.解决思路
- 首先需要保证A系统和B系统的网络是互通的:在A系统的服务器中pingB系统的ip
ping [服务器ip]
- 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失败')
}
}
- 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中
这个配置通常是在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;
}
}