转载自:半城云 Techer 群
作者:半城云技术赋能组组长 吴泫霖
UEditor 是一个富文本编辑器在公司的业务中广泛应用, 但由于浏览器的同源策略导致了一个 cross-iframe 的问题, 此文章来探讨这个问题以及分享一个解决方案
同源策略是现代浏览器的一个重要的安全机制, 我们遇到的场景是在dev-zhike.banchengyun.com
下要把 UEditor 作为 iframe 嵌入, 而dev-zhike.banchengyun.com
域名指向的是我们的服务器, 而 UEditor 则是放在 OSS 上以 CDN 域名访问, 如果我们直接嵌入会遇到这个错误
复制 Uncaught DOMException: Blocked a frame with origin “https://web-static.cdn.banchengyun.com” from accessing a cross-origin frame
原因是浏览器不允许嵌入一个不同源(origin)的 iframe, 解决这个问题的方法只有让当前页面和 iframe 是在同一个源下, 这篇文章重点讨论解决思路和方案, 具体的同源机制和 cross-iframe 问题自行百度
实现这个方案看起来也非常简单, 只需要把 UEditor 的文件放到服务器下就可以使用dev-zhike.banchengyun.com
的域名访问了, 如果这么实现那浏览器在加载 UEditor 的资源文件时流量就会从服务器的出口流出, 而服务器的出口带宽是非常珍贵的, 而且现有的服务器架构也不支持这种方案, 因为服务器只运行了后端的服务, 图片以及前端的资源文件都是存放在 OSS 上的
既然资源文件不能放到服务器上, 那可以在服务器上搭建一个反向代理把客户端请求代理到 OSS 上
对应的 nginx 配置如下
复制 server {
listen 80;
server_name dev-zhike.banchengyun.com;
location ~ /ueditor/(.*)$ {
proxy_pass https://web-static.cdn.banchengyun.com/$1;
}
}
这个方案可以实现使用dev-zhike.banchengyun.com
域名访问 UEditor 资源文件, 但是如上文所述浏览器加载资源的流量还是会从服务器出口流出, 所以这个方案没有完全解决问题
转换一下思路, 浏览器加载文件是会追随跳转的, 比如有http://foo.com/1.js
重定向到http://bar.com/1.js
, 使用script
标签加载http://foo.com/1.js
时浏览器会自动追随跳转到http://bar.com/1.js
, 应用到上面的场景可以想到dev-zhike.banchengyun.com
的域名只返回 301 跳转指示让浏览器去加载 OSS 的文件
复制 浏览器 -> nginx
<- 301 location: https://web-static.cdn.banchengyun.com/ue.js
-> Get https://web-static.cdn.banchengyun.com/ue.js
对应的 nginx 配置如下
复制 server {
listen 80;
server_name dev-zhike.banchengyun.com;
location /ueditor {
rewrite /ueditor/(.*)$ https://web-static.cdn.banchengyun.com/$1 permanent;
}
}
此时访问https://dev-zhike.banchengyun.com/ueditor/all.js
, nginx 会返回以下响应
复制 content-type : text/html
date : Wed, 25 Aug 2021 10:28:14 GMT
location : https://web-static.cdn.banchengyun.com/ueditor/all.js
server : nginx/1.20.0
content-length : 169
< html >
< head >< title >301 Moved Permanently</ title ></ head >
< body >
< center >< h1 >301 Moved Permanently</ h1 ></ center >
< hr >< center >nginx/1.20.0</ center >
</ body >
</ html >
这样就减小了服务器的出口流量, 但是这并没有完全解决问题, 在dev-zhike.banchengyun.com
以下面的代码嵌入 iframe 还是会遇到问题
复制 <iframe src="https://dev-zhike.banchengyun.com/index.html"></iframe>
第一个问题是浏览器会自动下载index.html
这个文件, 原因是 OSS 在处理html
文件会强制加入content-disposition attachment
这个响应头, 这个头会触发浏览器的下载行为, 由于用的是 rewrite 指令 nginx 没办法脱掉这个头, 所以需要对 html 文件做一下特殊处理, 改用proxy_pass
指令进行反向代理然后脱掉这个头再把内容返回给浏览器
复制 server {
listen 80;
server_name dev-zhike.banchengyun.com;
location ~ .html$ {
proxy_hide_header Content-Disposition;
proxy_pass https://web-static.cdn.banchengyun.com$uri;
}
location /ueditor {
rewrite /ueditor/(.*)$ https://web-static.cdn.banchengyun.com/$1 permanent;
}
}
这样.html 后缀的文件就会命中第一个location
块, 上面说到反向代理的流量是要经由服务器出口流出的, 但是 html 文件体积在可接受范围内所以可以忽略这个问题
但是问题并没有完全解决, 此时浏览器报错
复制 Uncaught DOMException: Blocked a frame with origin “https://dev-zhike.banchengyun.com” from accessing a cross-origin frame
观察一下报错的内容发现域名和一开始的不一样了, 这个问题还是浏览器的安全策略导致的, 默认情况下一个网页是不允许以 iframe 方式嵌入到另外一个网页的, 比如无法将百度作为 iframe 嵌入到我们的页面中, 但是有一个 HTTP 头能修改这个行为, 即X-Frame-Options 头, 这个头有两种取值: 第一种是deny
, 也就是拒绝所有嵌入行为也是默认行为, 第二种是sameorigin
, 允许同源嵌入, 只需要在location ~ .html$
块加入add_header X-Frame-Options sameorigin always;
指令即可解决问题
最后贴上实际场景的 nginx 配置
复制 map $http_host $ue_path {
dev-zhike.banchengyun.com scrm/public/js/ueditor-v1;
zhike.banchengyun.com scrm/public/js/ueditor-v1;
dev-yunying.banchengyun.com mall/public/ueditorV6;
yunying.banchengyun.com mall/public/ueditorV6;
pre-yunying.banchengyun.com mall/public/ueditorV6;
default "";
}
server {
listen 80;
resolver 114.114.114.114;
location ~ .html$ {
if ( $ue_path = "" ) {
return 404;
}
proxy_hide_header Content-Disposition;
add_header X-Frame-Options sameorigin always;
proxy_pass "https://bcy-web-static.oss-cn-hangzhou-internal.aliyuncs.com/$ue_path$uri";
}
location / {
if ( $ue_path = "" ) {
return 404;
}
rewrite . "https://web-static.cdn.banchengyun.com/$ue_path$uri" permanent;
}
}
可以看到与上面分析的差不多, 只是多了一个map
结构, 因为不同项目引入的 UEditor 是放在不同的 OSS 位置的