百度 UEditor 引发的 cross-iframe 问题解决方案

转载自:半城云 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 -> 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 位置的

最后更新于