iframe 跨域问题:代理方案与网络基础
文档概述
本文档基于实际项目经验,讲解 iframe 跨域问题的根本原因、代理解决方案(Vite/Nginx)、Cookie 携带配置,以及必要的网络基础知识(NAT、公网/内网等)。
适用场景:
- 新旧系统集成(通过 iframe 嵌入旧系统)
- 跨域资源访问
- 多环境部署配置(本地开发、测试、生产)
- 内网与公网混合架构
技术栈:
- 前端:Vue 3 + Vite
- 服务器:Nginx
- 部署:阿里云 ECS
第一章:问题现象与分析
1.1 问题描述
在开发过程中遇到以下问题场景:
场景:新系统(Vue3 前端)需要通过 iframe 嵌入旧系统页面
问题现象:
- 直接访问旧系统地址:
http://192.168.1.100:8080/app/page - iframe 页面加载后,旧系统内部的 AJAX 请求报错
- 第一个登录接口成功,第二个业务接口报跨域错误
- 浏览器提示:"此连接已被阻止,因为它是公共页面发起的,旨在连接到您本地网络上的设备或服务器"
1.2 错误信息分析
跨域错误(CORS Error)
Access to XMLHttpRequest at 'http://192.168.1.100:8080/api/xxx'
from origin 'http://localhost:5173' has been blocked by CORS policy
原因:浏览器的同源策略(Same-Origin Policy)限制
Private Network Access 错误
此连接已被阻止,因为它是公共页面发起的,
旨在连接到您本地网络上的设备或服务器
原因:Chrome 浏览器的 Private Network Access (PNA) 策略,防止公网页面访问本地/内网资源
1.3 问题根本原因
同源策略限制
浏览器判断两个 URL 是否同源的标准:
| 要素 | 说明 |
|---|---|
| 协议 | http vs https |
| 域名 | localhost vs 192.168.1.100 |
| 端口 | 5173 vs 8080 |
三者必须完全相同才算同源
示例:
父页面:http://localhost:5173/test/old-system-iframe
iframe:http://192.168.1.100:8080/app/page
↑ ↑
域名不同 端口不同
结论:不同源 → 跨域限制
Cookie 传递问题
即使后端配置了 CORS 响应头,Cookie 仍然可能无法携带:
第一次请求(登录):
→ 后端返回 Set-Cookie: sessionId=xxx; SameSite=Lax
→ 浏览器保存 Cookie
第二次请求(业务接口):
→ 浏览器发现是跨域请求
→ SameSite=Lax 不允许跨域携带 Cookie
→ 请求不带 Cookie → 后端认为未登录 → 403/401
第二章:浏览器安全策略详解
2.1 跨域资源共享(CORS)
CORS 基本原理
浏览器发起跨域请求
↓
1. 简单请求:直接发送,检查响应头
2. 预检请求:先发 OPTIONS,通过后再发实际请求
↓
服务器返回响应头
↓
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
↓
浏览器检查响应头
↓
符合 → 允许访问
不符合 → 阻止,报 CORS 错误
CORS 配置示例
后端配置(Java/Spring Boot):
java
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有来源(生产环境应指定具体域名)
config.addAllowedOriginPattern("*");
// 允许所有HTTP方法
config.addAllowedMethod("*");
// 允许所有请求头
config.addAllowedHeader("*");
// 允许携带Cookie
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
后端配置(Node.js/Express):
javascript
const cors = require('cors')
app.use(
cors({
origin: '*', // 生产环境应指定具体域名
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}),
)
2.2 Cookie 的 SameSite 属性
SameSite 属性说明
| 值 | 说明 | 跨域携带 | 安全性 |
|---|---|---|---|
| Strict | 完全禁止跨站发送 | ❌ 否 | 最高 |
| Lax | 部分允许(默认值) | ⚠️ 仅顶级导航 | 中等 |
| None | 允许跨站发送 | ✅ 是(需配合Secure) | 较低 |
SameSite=Lax 的限制
默认情况下,浏览器设置 Cookie 时使用 SameSite=Lax:
场景1:用户点击链接
<a href="https://other-site.com">链接</a>
→ Cookie 会携带 ✅
场景2:iframe 嵌入
<iframe src="https://other-site.com"></iframe>
→ Cookie 不会携带 ❌
场景3:AJAX 请求
fetch('https://other-site.com/api')
→ Cookie 不会携带 ❌
SameSite=None 的要求
要允许 iframe 或 AJAX 跨域携带 Cookie,必须:
Set-Cookie: sessionId=xxx; SameSite=None; Secure
↑ ↑
允许跨域 必须HTTPS
注意 :SameSite=None 必须配合 Secure 属性,意味着必须在 HTTPS 环境下使用。
浏览器兼容性
Chrome 80+ (2020年2月):默认 SameSite=Lax
Firefox 69+ (2019年9月):默认 SameSite=Lax
Safari:更严格,完全阻止第三方Cookie
Edge:跟随 Chrome
2.3 Private Network Access (PNA) 策略
PNA 策略目的
防止公网页面攻击内网设备:
恶意网站(公网)
↓ 想访问
用户的路由器管理页面(192.168.1.1)
用户的打印机(192.168.1.100)
用户的 NAS(192.168.1.200)
↓
PNA 策略阻止 ❌
触发条件
条件1:页面来源是公网地址
条件2:请求目标是私有网络地址(10.x, 172.16-31.x, 192.168.x)
条件3:没有正确的 CORS 响应头
↓
浏览器阻止请求
解决方案
后端添加响应头:
Access-Control-Allow-Private-Network: true
配合标准的 CORS 响应头一起使用。
第三章:代理方案详解
3.1 代理的本质
什么是代理
代理本质上是"请求转发":
客户端 → 发送请求到代理服务器 → 代理转发到目标服务器
↑ ↑
同源(无跨域) 可能跨域,但服务器之间无限制
↓
返回响应
↓
客户端 ← 代理返回响应
关键点:
- 浏览器只看到与代理服务器的交互(同源)
- 代理服务器与目标服务器的交互不受浏览器限制
- Cookie 会自动携带(因为浏览器认为是同源)
代理 vs CORS
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| CORS | 后端添加响应头 | 直接访问,性能好 | Cookie携带复杂,浏览器限制多 |
| 代理 | 请求转发 | 彻底解决跨域,Cookie自动携带 | 多一层转发,配置相对复杂 |
3.2 Vite 代理配置(开发环境)
基本配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
// 代理旧系统
'/old-system': {
target: 'http://192.168.1.100:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/old-system/, ''),
},
},
},
})
配置详解
typescript
'/old-system': {
// 目标服务器地址
target: 'http://192.168.1.100:8080',
// 改变请求头中的 origin 为目标地址
changeOrigin: true,
// 路径重写:/old-system/api → /api
rewrite: (path) => path.replace(/^\/old-system/, ''),
// 代理日志(调试用)
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
console.log('[Proxy]', req.method, req.url, '→', proxyReq.path)
})
proxy.on('error', (err) => {
console.error('[Proxy Error]', err)
})
}
}
请求流程
浏览器发起请求
↓
http://localhost:5173/old-system/app/page
↓ Vite 拦截 /old-system 开头的请求
↓ 转发到目标服务器
http://192.168.1.100:8080/app/page
↓ 目标服务器返回响应
↓ Vite 转发回浏览器
浏览器收到响应
对浏览器而言:
- 请求地址:
http://localhost:5173/old-system/... - 完全同源,无跨域限制
- Cookie 自动携带
Cookie 重写配置
如果后端 Cookie 的 SameSite 设置不当,可以在代理层重写:
typescript
'/old-system': {
target: 'http://192.168.1.100:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/old-system/, ''),
configure: (proxy) => {
// 修改响应的 Set-Cookie 头
proxy.on('proxyRes', (proxyRes) => {
const setCookieHeaders = proxyRes.headers['set-cookie']
if (setCookieHeaders) {
proxyRes.headers['set-cookie'] = setCookieHeaders.map((cookie) => {
// 将 SameSite=Lax 替换为 SameSite=None
let modified = cookie.replace(/SameSite=Lax/gi, 'SameSite=None')
// 添加 SameSite=None
if (!modified.includes('SameSite')) {
modified += '; SameSite=None'
}
// SameSite=None 必须配合 Secure
if (!modified.includes('Secure')) {
modified += '; Secure'
}
return modified
})
}
})
}
}
注意:修改 SameSite 会降低 CSRF 防护,仅在开发环境或内部系统使用。
3.3 Nginx 代理配置(测试/生产环境)
基本配置
nginx
server {
listen 8082;
server_name _;
# 前端静态文件
root /opt/soft/app-client;
index index.html;
# 代理后端 API
location /api/ {
proxy_pass http://203.0.113.10:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
}
# 代理旧系统(解决 iframe 跨域)
location /old-system/ {
proxy_pass http://192.168.1.100:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
# Cookie 处理(可选)
# proxy_cookie_domain 192.168.1.100 $host;
# proxy_cookie_path / /old-system/;
}
# Vue Router history 模式支持
location / {
try_files $uri $uri/ /index.html;
}
}
配置说明
nginx
location /old-system/ {
# 目标服务器地址(注意末尾的 /)
proxy_pass http://192.168.1.100:8080/;
# 传递原始 Host 头(重要)
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递 Cookie(重要)
proxy_set_header Cookie $http_cookie;
# 使用 HTTP/1.1(支持 keep-alive)
proxy_http_version 1.1;
}
Cookie Domain 重写
如果后端设置的 Cookie 带有 Domain 属性,需要重写:
nginx
location /old-system/ {
proxy_pass http://192.168.1.100:8080/;
# 重写 Cookie 的 Domain
# 将后端的 Domain 改为当前域名
proxy_cookie_domain 192.168.1.100 $host;
# 重写 Cookie 的 Path
# 将后端的 Path=/ 改为 Path=/old-system/
proxy_cookie_path / /old-system/;
}
配置生效
bash
# 测试配置是否正确
nginx -t
# 重新加载配置(不中断服务)
nginx -s reload
# 重启 Nginx(会中断服务)
systemctl restart nginx
3.4 前端代码配置
iframe 使用代理路径
vue
<!-- src/views/test/OldSystemIframe.vue -->
<template>
<div class="old-system-iframe">
<div class="iframe-wrapper">
<!-- 使用代理路径,不是直接地址 -->
<iframe src="/old-system/app/page" frameborder="0" title="旧系统" />
</div>
</div>
</template>
<style scoped>
.old-system-iframe {
width: 100%;
height: 100%;
}
.iframe-wrapper {
width: 100%;
height: 100%;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
动态切换环境
如果需要在不同环境使用不同地址:
typescript
// src/config/env.ts
export const OLD_SYSTEM_BASE_URL = import.meta.env.VITE_OLD_SYSTEM_BASE_URL || '/old-system'
// .env.development
VITE_OLD_SYSTEM_BASE_URL=/old-system
// .env.production
VITE_OLD_SYSTEM_BASE_URL=/old-system
vue
<script setup lang="ts">
import { OLD_SYSTEM_BASE_URL } from '@/config/env'
const iframeSrc = `${OLD_SYSTEM_BASE_URL}/app/page`
</script>
<template>
<iframe :src="iframeSrc" />
</template>
第四章:网络架构基础
4.1 公网 IP vs 内网 IP
IP 地址分类
公网 IP(Public IP):
- 全球唯一的地址
- 可以在互联网上直接访问
- 示例:
203.0.113.10、8.8.8.8
内网 IP(Private IP):
- 仅在局域网内有效
- 不能在互联网上直接访问
- 可以重复使用(不同局域网可以有相同的内网IP)
私有 IP 地址段(RFC 1918)
A 类:10.0.0.0 - 10.255.255.255 (16,777,216 个地址)
B 类:172.16.0.0 - 172.31.255.255 (1,048,576 个地址)
C 类:192.168.0.0 - 192.168.255.255 (65,536 个地址)
判断示例:
203.0.113.10 → 公网 IP ✅
192.168.1.100 → 内网 IP(C类)
172.16.0.10 → 内网 IP(B类,阿里云内网)
10.0.1.100 → 内网 IP(A类)
8.8.8.8 → 公网 IP(Google DNS)
为什么需要内网 IP
IPv4 地址总数:2^32 = 4,294,967,296 个(约 43 亿)
全球设备数量:远超 43 亿
解决方案:
└─ NAT(网络地址转换)
└─ 一个公网 IP + 多个内网 IP
└─ 节省公网 IP 资源
4.2 NAT(网络地址转换)原理
NAT 工作流程
内网设备发起请求
↓
【内网设备】192.168.1.100:50000
↓ 发送请求到 8.8.8.8:53
↓
【路由器/网关】
↓ NAT 转换
├─ 源地址:192.168.1.100:50000 → 公网IP:12345
├─ 目标地址:8.8.8.8:53(不变)
├─ 记录映射:公网IP:12345 ←→ 192.168.1.100:50000
↓
【公网】请求从 公网IP:12345 发出
↓
【目标服务器】8.8.8.8:53 收到请求
↓ 返回响应到 公网IP:12345
↓
【路由器/网关】
↓ 根据映射表查找
├─ 公网IP:12345 → 对应 → 192.168.1.100:50000
↓ 转换后转发
↓
【内网设备】192.168.1.100:50000 收到响应
NAT 映射表示例
| 内网地址 | 内网端口 | 公网端口 | 目标地址 | 目标端口 | 状态 |
|---|---|---|---|---|---|
| 192.168.1.100 | 50000 | 12345 | 8.8.8.8 | 53 | 活跃 |
| 192.168.1.101 | 51234 | 12346 | 1.1.1.1 | 443 | 活跃 |
| 192.168.1.100 | 50001 | 12347 | 203.0.113.10 | 80 | 活跃 |
内网 → 公网 ✅(可以)
内网主动发起连接
↓
NAT 记录映射
↓
公网响应
↓
NAT 根据映射转发回内网
↓
✅ 成功通信
关键:内网主动发起,NAT 知道怎么转发回复
公网 → 内网 ❌(不可以)
公网主动发起连接
↓
NAT 收到请求
↓
问题:没有映射记录,不知道转发给哪台内网设备
↓
❌ 丢弃请求 或 拒绝连接
关键:没有预先建立的连接,NAT 不知道怎么转发
4.3 内网穿透方案
如果确实需要公网访问内网,有以下方案:
方案1:端口映射(Port Forwarding)
在路由器上配置:
配置项:
外部端口:8080
内部 IP:192.168.1.100
内部端口:8080
协议:TCP
效果:
公网IP:8080 → 自动转发到 → 192.168.1.100:8080
优点:
- 简单直接
- 性能好(直接转发)
缺点:
- 需要路由器配置权限
- 暴露端口有安全风险
- 每个服务都要单独配置
方案2:内网穿透工具(frp/ngrok)
原理:
【内网机器】
↓ 主动连接并保持
【公网服务器(frp server)】
↑ 长连接保持
↓ 接收外部请求
【外部访问者】
↓ 访问 public.example.com
【frp server】
↓ 通过已建立的连接转发
【内网机器】处理请求并返回
frp 配置示例:
服务端(公网服务器):
ini
# frps.ini
[common]
bind_port = 7000
客户端(内网机器):
ini
# frpc.ini
[common]
server_addr = 公网服务器IP
server_port = 7000
[old-system]
type = http
local_ip = 127.0.0.1
local_port = 8080
custom_domains = old-system.example.com
优点:
- 无需路由器配置
- 支持多种协议
- 可以添加认证
缺点:
- 需要额外的服务器
- 性能有一定损耗
- 依赖第三方服务
方案3:VPN
【公网机器】
↓ 连接 VPN
【VPN 服务器】
↓ 虚拟分配内网 IP
【公网机器】获得虚拟内网 IP(如 10.0.0.100)
↓ 现在可以访问内网资源
【内网机器】192.168.1.100
优点:
- 可以访问整个内网
- 安全性高(加密)
- 适合多种场景
缺点:
- 需要 VPN 服务器
- 配置相对复杂
- 客户端需要安装 VPN 软件
第五章:多环境配置实践
5.1 环境划分
典型的三环境架构:
| 环境 | 用途 | 网络环境 | 配置 |
|---|---|---|---|
| 开发环境 | 本地开发调试 | 办公室局域网 | Vite 代理 |
| 测试环境 | 功能测试、集成测试 | 云服务器(公网) | Nginx 代理 |
| 生产环境 | 正式服务 | 云服务器(公网) | Nginx 代理 + HTTPS |
5.2 开发环境配置
目录结构
project/
├── vite.config.ts # Vite 配置
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
└── src/
├── config/
│ └── env.ts # 环境配置
└── views/
└── test/
└── OldSystemIframe.vue
vite.config.ts
typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
host: '0.0.0.0',
port: 5173,
cors: true,
proxy: {
// 代理新系统后端 API
'/api': {
target: 'http://192.168.1.100:8083', // 局域网后端
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 代理旧系统(解决跨域和 Cookie 问题)
'/old-system': {
target: 'http://192.168.1.100:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/old-system/, ''),
configure: (proxy) => {
// 请求日志
proxy.on('proxyReq', (proxyReq, req) => {
console.log('[Old System Proxy]', req.method, req.url, '→', proxyReq.path)
})
// 修改响应的 Set-Cookie(开发环境)
proxy.on('proxyRes', (proxyRes) => {
const setCookieHeaders = proxyRes.headers['set-cookie']
if (setCookieHeaders) {
proxyRes.headers['set-cookie'] = setCookieHeaders.map((cookie) => {
let modified = cookie.replace(/SameSite=Lax/gi, 'SameSite=None')
if (!modified.includes('SameSite')) {
modified += '; SameSite=None'
}
if (!modified.includes('Secure')) {
modified += '; Secure'
}
return modified
})
}
})
// 错误处理
proxy.on('error', (err) => {
console.error('[Old System Proxy Error]', err)
})
},
},
},
},
})
环境变量
bash
# .env.development
VITE_APP_TITLE=App系统(开发)
VITE_API_BASE_URL=/api
VITE_OLD_SYSTEM_BASE_URL=/old-system
bash
# .env.production
VITE_APP_TITLE=App系统
VITE_API_BASE_URL=/api
VITE_OLD_SYSTEM_BASE_URL=/old-system
5.3 测试环境配置
Nginx 配置
nginx
# /opt/soft/nginx/conf/nginx.conf
server {
listen 8082;
server_name test.example.com;
# 前端静态文件
root /opt/soft/app-client;
index index.html;
# 代理新系统后端 API
location /api/ {
proxy_pass http://203.0.113.10:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
}
# 代理旧系统(需要确保测试服务器能访问)
location /old-system/ {
# 注意:这里的地址必须是测试服务器能访问的
# 如果是内网地址,需要确保在同一网络
# 或者改为旧系统的测试环境地址
proxy_pass http://旧系统测试地址:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
# Cookie 处理
proxy_cookie_domain 旧系统域名 $host;
}
# Vue Router history 模式支持
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
}
部署脚本
bash
#!/bin/bash
# deploy-test.sh
echo "开始部署测试环境..."
# 1. 构建前端
echo "1. 构建前端..."
npm run build
# 2. 备份旧文件
echo "2. 备份旧文件..."
ssh root@203.0.113.10 "cd /opt/soft && cp -r app-client app-client.backup.$(date +%Y%m%d%H%M%S)"
# 3. 上传新文件
echo "3. 上传新文件..."
scp -r dist/* root@203.0.113.10:/opt/soft/app-client/
# 4. 重启 Nginx
echo "4. 重新加载 Nginx..."
ssh root@203.0.113.10 "nginx -t && nginx -s reload"
echo "部署完成!"
echo "访问地址:http://203.0.113.10:8082"
5.4 生产环境配置
Nginx 配置(HTTPS)
nginx
# 生产环境配置
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name prod.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS 配置
server {
listen 443 ssl http2;
server_name prod.example.com;
# SSL 证书
ssl_certificate /etc/nginx/ssl/prod.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/prod.example.com.key;
# SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 前端静态文件
root /opt/soft/app-client;
index index.html;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
# 代理新系统后端 API
location /api/ {
proxy_pass https://prod-backend.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 代理旧系统
location /old-system/ {
proxy_pass https://old-system-prod.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
# Cookie 处理
proxy_cookie_domain old-system-prod.example.com $host;
proxy_cookie_flags ~ secure httponly samesite=none;
}
# Vue Router history 模式
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存(更长的过期时间)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 日志
access_log /var/log/nginx/prod.access.log;
error_log /var/log/nginx/prod.error.log;
}
第六章:问题排查与调试
6.1 网络连通性测试
测试工具对比
| 工具 | 协议 | 用途 | 示例 |
|---|---|---|---|
| ping | ICMP | 测试网络可达性 | ping 192.168.1.100 |
| curl | HTTP | 测试 HTTP 服务 | curl -I http://192.168.1.100:8080 |
| telnet | TCP | 测试端口连通性 | telnet 192.168.1.100 8080 |
| nc | TCP/UDP | 多功能网络工具 | nc -zv 192.168.1.100 8080 |
ping vs HTTP 请求
ping 测试
└─ 使用 ICMP 协议
└─ 不经过端口
└─ 容易被防火墙阻止
└─ ping 不通 ≠ HTTP 访问不通
HTTP 请求(curl/浏览器)
└─ 使用 HTTP 协议(TCP)
└─ 经过具体端口(如 8080)
└─ 更接近实际应用场景
└─ 能测试服务是否正常
示例:
bash
# 1. ping 测试(可能被防火墙阻止)
ping 192.168.1.100
# 结果:请求超时
# 2. curl 测试(HTTP 协议)
curl -I http://192.168.1.100:8080/app/page
# 结果:HTTP/1.1 200 OK
# 说明:服务正常,只是 ICMP 被阻止了
6.2 浏览器开发者工具
Network 面板关键信息
Network 面板
├─ Request URL:请求的完整 URL
│ └─ 检查是否走了代理路径
│
├─ Status:响应状态码
│ ├─ 200:成功
│ ├─ 401/403:认证/授权失败(Cookie 问题)
│ ├─ 404:路径错误
│ ├─ 502/504:代理后端无响应
│ └─ (failed) CORS error:跨域错误
│
├─ Request Headers:请求头
│ ├─ Origin:请求来源
│ ├─ Referer:引用页面
│ └─ Cookie:是否携带 Cookie ⭐
│
└─ Response Headers:响应头
├─ Access-Control-Allow-Origin:CORS 配置
├─ Set-Cookie:服务器设置的 Cookie ⭐
└─ Content-Type:响应类型
检查 Cookie 携带
步骤:
- F12 打开开发者工具
- 切换到 Network 标签
- 勾选 "Preserve log"
- 刷新页面或触发请求
- 找到目标请求,点击查看详情
- 查看 Request Headers → 找
Cookie:字段
判断:
✅ 有 Cookie 字段
Cookie: sessionId=xxx; userId=yyy
→ Cookie 成功携带
❌ 没有 Cookie 字段
→ Cookie 未携带,需要检查:
1. 是否跨域?
2. SameSite 属性是否正确?
3. 代理配置是否正确?
检查 Set-Cookie
步骤:
- Network 标签中找到登录请求
- 查看 Response Headers
- 找
Set-Cookie:字段 - 检查属性
重点关注:
Set-Cookie: sessionId=xxx; Path=/; HttpOnly; SameSite=Lax
↑
检查这个属性
SameSite=Strict → iframe 不会携带 ❌
SameSite=Lax → iframe 不会携带 ❌
SameSite=None → iframe 会携带 ✅(需配合 Secure)
查看 Cookie 存储
Application 面板:
- F12 → Application 标签
- 左侧展开 Cookies
- 点击域名查看所有 Cookie
- 检查:
- Name:Cookie 名称
- Value:Cookie 值
- Domain:作用域
- Path:路径
- Expires:过期时间
- HttpOnly:是否仅HTTP
- Secure:是否仅HTTPS
- SameSite:同站策略
6.3 Nginx 日志分析
访问日志
bash
# 实时查看访问日志
tail -f /var/log/nginx/access.log
# 或者项目特定的日志
tail -f /opt/soft/nginx/logs/access.log
日志格式:
203.0.113.20 - - [15/Jan/2026:17:41:12 +0800]
"GET /old-system/app/page HTTP/1.1" 200 15234
"http://203.0.113.10:8082/test/old-system-iframe"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/143.0.0.0"
分析要点:
- 客户端 IP
- 请求时间
- 请求方法和路径
- 状态码(200/404/502等)
- 响应大小
- Referer(请求来源)
- User-Agent(浏览器信息)
错误日志
bash
# 实时查看错误日志
tail -f /var/log/nginx/error.log
常见错误:
# 后端连接超时
upstream timed out (110: Connection timed out)
while connecting to upstream
→ 后端服务器无响应或不可达
# 后端拒绝连接
connect() failed (111: Connection refused)
while connecting to upstream
→ 后端服务未启动或端口错误
# 找不到上游服务器
no resolver defined to resolve xxx
→ DNS 解析问题
# 权限问题
open() "/path/to/file" failed (13: Permission denied)
→ 文件权限不足
6.4 常见问题排查流程
问题1:请求跨域
现象:
Console 报错:
Access to XMLHttpRequest at 'http://...' from origin 'http://...'
has been blocked by CORS policy
排查步骤:
1. 检查 Request URL
└─ 是否走了代理路径?
✅ /old-system/xxx → 正确
❌ http://192.168.x.x/xxx → 没走代理
2. 如果没走代理
└─ 检查前端代码 iframe src
└─ 检查 Vite/Nginx 配置
└─ 重启服务
3. 如果走了代理还跨域
└─ 检查后端 CORS 配置
└─ 添加必要的响应头
问题2:Cookie 未携带
现象:
第一个请求(登录):成功
第二个请求(业务接口):401/403 未授权
排查步骤:
1. 检查 Network → Request Headers
└─ 有 Cookie 字段吗?
❌ 没有 → 继续排查
✅ 有但值不对 → 后端问题
2. 检查 Set-Cookie 响应头
└─ SameSite 属性是什么?
Lax/Strict → 需要改为 None
None → 检查是否有 Secure
3. 检查代理配置
└─ proxy_set_header Cookie 配置了吗?
└─ Cookie 重写配置正确吗?
4. 检查浏览器环境
└─ 是否是 HTTPS?(SameSite=None 需要)
└─ 浏览器是否阻止第三方 Cookie?
问题3:代理无法访问后端
现象:
Network 显示:(pending) 一直等待
或:502 Bad Gateway
排查步骤:
1. 确认代理服务器能访问后端
└─ SSH 登录到代理服务器
└─ 执行:curl -I http://后端地址
2. 检查网络可达性
└─ 公网服务器访问内网地址?
└─ ❌ 不可达
└─ 解决:使用测试环境地址或内网穿透
3. 检查后端服务状态
└─ 服务是否启动?
└─ 端口是否正确?
└─ 防火墙是否阻止?
4. 检查 Nginx 日志
└─ tail -f /var/log/nginx/error.log
└─ 查看具体错误信息
第七章:安全性考虑
7.1 CSRF 攻击与防护
CSRF 攻击原理
用户正常访问网站A
↓
登录成功,浏览器保存 Cookie
↓
用户在同一浏览器访问恶意网站B
↓
恶意网站B 的代码:
<form action="https://网站A/api/deleteUser" method="POST">
<input type="hidden" name="userId" value="123">
</form>
<script>document.forms[0].submit()</script>
↓
浏览器自动带上网站A的 Cookie 发送请求
↓
网站A 收到请求,验证 Cookie 通过
↓
❌ 执行了恶意操作(删除用户)
SameSite 的防护作用
SameSite=Lax/Strict
└─ 跨站请求不带 Cookie
└─ 恶意网站的表单提交不会带 Cookie
└─ ✅ 阻止 CSRF 攻击
SameSite=None
└─ 跨站请求会带 Cookie
└─ 恶意网站的请求也会带 Cookie
└─ ❌ 无法防止 CSRF
使用 SameSite=None 的风险
风险1:CSRF 攻击
└─ 解决:添加 CSRF Token
风险2:信息泄露
└─ 解决:敏感操作二次验证
风险3:会话劫持
└─ 解决:使用 HTTPS + Secure
CSRF Token 防护方案
原理:
1. 用户登录后,服务器生成随机 Token
2. Token 存储在服务器 Session 或数据库
3. Token 返回给前端(非 Cookie 方式)
4. 前端每次请求都带上 Token(通常在请求头)
5. 服务器验证 Token 是否有效
实现示例:
后端(Java/Spring):
java
@RestController
public class AuthController {
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
// 验证用户名密码
User user = authService.authenticate(request.getUsername(), request.getPassword());
// 生成 Session 和 CSRF Token
String sessionId = UUID.randomUUID().toString();
String csrfToken = UUID.randomUUID().toString();
// 存储到 Redis
redisTemplate.opsForValue().set("session:" + sessionId, user.getId());
redisTemplate.opsForValue().set("csrf:" + sessionId, csrfToken);
// 设置 Cookie(SameSite=None)
Cookie cookie = new Cookie("sessionId", sessionId);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setAttribute("SameSite", "None");
response.addCookie(cookie);
// 返回 CSRF Token(通过响应体)
return new LoginResponse(user, csrfToken);
}
@PostMapping("/api/protected")
public Response protectedOperation(
@CookieValue("sessionId") String sessionId,
@RequestHeader("X-CSRF-Token") String csrfToken
) {
// 验证 CSRF Token
String expectedToken = redisTemplate.opsForValue().get("csrf:" + sessionId);
if (!csrfToken.equals(expectedToken)) {
throw new SecurityException("Invalid CSRF token");
}
// 执行操作
return doProtectedOperation();
}
}
前端(Vue):
typescript
// 登录后保存 CSRF Token
const login = async (username: string, password: string) => {
const response = await axios.post('/api/login', { username, password })
// CSRF Token 存储在 localStorage(不是 Cookie)
localStorage.setItem('csrfToken', response.data.csrfToken)
}
// 请求拦截器:自动添加 CSRF Token
axios.interceptors.request.use((config) => {
const csrfToken = localStorage.getItem('csrfToken')
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken
}
return config
})
// 业务请求
const deleteUser = async (userId: string) => {
// CSRF Token 会自动添加到请求头
await axios.post('/api/deleteUser', { userId })
}
7.2 XSS 攻击防护
iframe 嵌入场景的 XSS 风险
├─ 旧系统可能有 XSS 漏洞
├─ 通过 iframe 注入恶意脚本
└─ 窃取新系统的数据
防护措施:
├─ Content-Security-Policy(CSP)
├─ X-Frame-Options
└─ sandbox 属性
CSP 配置:
nginx
# Nginx 配置
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
frame-src 'self' https://trusted-old-system.com;
";
iframe sandbox:
html
<!-- 限制 iframe 的能力 -->
<iframe src="/old-system/app/page" sandbox="allow-scripts allow-same-origin allow-forms"> </iframe>
<!-- sandbox 属性说明:
allow-scripts: 允许执行脚本
allow-same-origin: 允许同源访问
allow-forms: 允许表单提交
allow-top-navigation: 允许顶层导航(谨慎使用)
-->
7.3 HTTPS 最佳实践
为什么需要 HTTPS
1. SameSite=None 必须配合 Secure(HTTPS)
2. 防止中间人攻击
3. 保护敏感数据传输
4. 提升 SEO 排名
5. 浏览器安全提示
SSL/TLS 配置
nginx
server {
listen 443 ssl http2;
server_name prod.example.com;
# SSL 证书(推荐使用 Let's Encrypt)
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# SSL 协议(禁用不安全的版本)
ssl_protocols TLSv1.2 TLSv1.3;
# 加密套件(优先使用安全的)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# Session 缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS(强制 HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# OCSP Stapling(提升 SSL 性能)
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
}
第八章:性能优化
8.1 Nginx 优化
nginx
# 性能优化配置
# 工作进程数(通常设置为 CPU 核心数)
worker_processes auto;
# 每个进程的最大连接数
events {
worker_connections 4096;
use epoll; # Linux 下使用 epoll
}
http {
# 开启 Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;
# 文件描述符缓存
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 代理缓冲
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 32 8k;
proxy_busy_buffers_size 16k;
# 代理超时
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
8.2 前端优化
代码分割
typescript
// 路由懒加载
const routes = [
{
path: '/test/old-system-iframe',
component: () => import('@/views/test/OldSystemIframe.vue'),
},
]
资源预加载
html
<!-- index.html -->
<head>
<!-- 预连接到旧系统 -->
<link rel="preconnect" href="https://old-system.example.com" />
<link rel="dns-prefetch" href="https://old-system.example.com" />
</head>
iframe 延迟加载
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const showIframe = ref(false)
// 页面加载完成后再加载 iframe
onMounted(() => {
setTimeout(() => {
showIframe.value = true
}, 500)
})
</script>
<template>
<div class="iframe-wrapper">
<iframe v-if="showIframe" src="/old-system/app/page" loading="lazy" />
<div v-else class="loading">加载中...</div>
</div>
</template>
第九章:监控与告警
9.1 Nginx 访问日志分析
日志格式定义
nginx
http {
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$upstream_addr $upstream_status';
access_log /var/log/nginx/access.log detailed;
}
关键指标监控
bash
# 实时监控代理请求
tail -f /var/log/nginx/access.log | grep '/old-system/'
# 统计状态码分布
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# 统计响应时间(找出慢请求)
awk '{if ($NF > 1) print $0}' /var/log/nginx/access.log
# 统计访问最多的 URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
性能监控
typescript
// 监控 iframe 加载时间
const monitorIframeLoad = () => {
const startTime = performance.now()
const iframe = document.querySelector('iframe')
if (iframe) {
iframe.addEventListener('load', () => {
const loadTime = performance.now() - startTime
console.log('Iframe load time:', loadTime, 'ms')
// 上报性能数据
reportPerformance({
type: 'iframe-load',
duration: loadTime,
url: iframe.src,
})
})
}
}
第十章:总结与最佳实践
10.1 问题解决方案总结
| 问题 | 开发环境方案 | 测试/生产环境方案 |
|---|---|---|
| 跨域 | Vite 代理 | Nginx 代理 |
| Cookie携带 | Cookie重写 + 代理 | 后端设置SameSite=None + Nginx代理 |
| 网络隔离 | 访问本地内网地址 | 使用测试环境地址或VPN |
| 安全性 | 相对宽松(开发调试) | HTTPS + CSRF Token + CSP |
10.2 技术选型建议
场景1:内部系统集成(推荐)
方案:代理方案
├─ 开发:Vite 代理
├─ 测试/生产:Nginx 代理
└─ 优点:
├─ 彻底解决跨域
├─ Cookie 自动携带
├─ 无需后端大量改动
└─ 统一域名,便于管理
场景2:第三方系统集成
方案:postMessage 通信
├─ iframe 嵌入第三方页面
├─ 通过 postMessage 传递数据
└─ 优点:
├─ 安全隔离
├─ 不依赖 Cookie
└─ 适合不可控的第三方系统
场景3:微前端架构
方案:qiankun 等微前端框架
├─ 子应用独立运行
├─ 主应用统一加载
└─ 优点:
├─ 技术栈无关
├─ 独立部署
├─ 样式隔离
└─ 适合大型多系统集成
10.3 配置清单
开发环境
✅ vite.config.ts 配置代理
✅ iframe 使用代理路径
✅ 重启 Vite 服务
✅ 清除浏览器缓存测试
测试环境
✅ Nginx 配置代理
✅ 确保测试服务器能访问后端
✅ 前端代码使用代理路径
✅ 重新打包部署
✅ 重载 Nginx 配置
✅ 清除浏览器缓存测试
生产环境
✅ 配置 HTTPS 和 SSL 证书
✅ Nginx 代理配置(使用生产地址)
✅ 后端设置 SameSite=None; Secure
✅ 添加 CSRF Token 验证
✅ 配置 CSP 安全头
✅ 性能优化(Gzip、缓存等)
✅ 配置监控和告警
✅ 完整测试验证
10.4 注意事项
安全性
⚠️ SameSite=None 会降低 CSRF 防护
└─ 解决:添加 CSRF Token 验证
⚠️ iframe 可能存在 XSS 风险
└─ 解决:使用 CSP 和 sandbox 属性
⚠️ 敏感信息不要存储在 Cookie
└─ 解决:使用 HttpOnly 和加密
⚠️ 生产环境必须使用 HTTPS
└─ 解决:申请 SSL 证书(Let's Encrypt 免费)
性能
⚠️ iframe 会增加页面加载时间
└─ 解决:延迟加载、预加载
⚠️ 代理会增加一层网络开销
└─ 解决:Nginx 配置优化、启用缓存
⚠️ Cookie 过大会影响性能
└─ 解决:精简 Cookie、使用 Session
兼容性
⚠️ SameSite=None 需要较新的浏览器
└─ 兼容:Chrome 80+, Firefox 69+, Safari 13+
⚠️ 部分浏览器完全阻止第三方 Cookie
└─ Safari 默认阻止,需要用户手动开启
⚠️ 某些企业浏览器策略可能更严格
└─ 解决:提供用户配置说明
10.5 故障处理预案
场景1:代理后端无响应
现象:502 Bad Gateway / 504 Gateway Timeout
排查:
1. 检查后端服务是否启动
2. 检查网络连通性(curl 测试)
3. 查看 Nginx error.log
4. 检查防火墙和安全组配置
临时方案:
1. 降级:显示友好提示,隐藏 iframe
2. 切换:使用备用地址
场景2:Cookie 突然失效
现象:用户频繁掉线、需要重新登录
排查:
1. 检查 Set-Cookie 响应头是否变化
2. 检查浏览器是否更新(策略变化)
3. 检查服务器时间(Cookie 过期)
4. 检查 Session 存储(Redis等)
解决:
1. 调整 Cookie 过期时间
2. 实现 Token 刷新机制
3. 添加"记住我"功能
场景3:特定用户无法访问
现象:部分用户报错,大部分用户正常
排查:
1. 浏览器版本和设置
2. 网络环境(VPN、代理)
3. 企业安全策略
4. 设备类型(移动端/PC)
解决:
1. 提供浏览器兼容性说明
2. 提供配置指南
3. 提供降级方案
附录
A. 术语表
| 术语 | 英文 | 说明 |
|---|---|---|
| 跨域 | Cross-Origin | 浏览器安全策略,限制不同源之间的资源访问 |
| 同源策略 | Same-Origin Policy | 协议、域名、端口完全相同才算同源 |
| CORS | Cross-Origin Resource Sharing | 跨域资源共享,允许服务器声明允许的跨域访问 |
| CSRF | Cross-Site Request Forgery | 跨站请求伪造攻击 |
| XSS | Cross-Site Scripting | 跨站脚本攻击 |
| NAT | Network Address Translation | 网络地址转换 |
| PNA | Private Network Access | 私有网络访问策略 |
| CSP | Content Security Policy | 内容安全策略 |
| HSTS | HTTP Strict Transport Security | HTTP 严格传输安全 |
B. 参考资源
浏览器安全:
- MDN Web Docs - Same-origin policy
- MDN Web Docs - Cross-Origin Resource Sharing (CORS)
- Chrome Platform Status - SameSite Cookies
网络基础:
- RFC 1918 - Address Allocation for Private Internets
- RFC 6749 - The OAuth 2.0 Authorization Framework
Nginx 文档:
- Nginx Official Documentation - http://nginx.org/en/docs/
- Nginx Proxy Module - http://nginx.org/en/docs/http/ngx_http_proxy_module.html
Vite 文档:
- Vite Official Documentation - https://vitejs.dev/
- Vite Server Options - https://vitejs.dev/config/server-options.html
C. 快速参考卡片
Vite 代理配置模板
typescript
export default defineConfig({
server: {
proxy: {
'/prefix': {
target: 'http://target-server:port',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/prefix/, ''),
},
},
},
})
Nginx 代理配置模板
nginx
location /prefix/ {
proxy_pass http://target-server:port/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
}
Cookie 配置参考
Set-Cookie: name=value;
Path=/;
Domain=.example.com;
HttpOnly;
Secure;
SameSite=None;
Max-Age=86400
文档结束
本文档基于真实项目经验总结,涵盖了 iframe 跨域问题的完整解决方案,从问题分析、技术原理、实施方案到安全性和性能优化,提供了全面的实践指导。希望对遇到类似问题的开发者有所帮助。