遇到的跨域场景(CORS/JSONP/代理/iframe 通信)总结

iframe 在同源和跨域的通信方式

同源

父页面和 iframe 可以直接访问对方的 DOM 和 JavaScript 对象。

这种方式简单高效,但需要注意代码的安全性和健壮性,避免产生错误或安全漏洞。

跨域

由于同源策略的限制,需要使用 window.postMessage() 方法进行通信。

同源情况下的通信

直接访问 iframe 的内容

从父页面访问 iframe

父页面可以通过 iframe 元素的 contentWindow 或 contentDocument 属性获取 iframe 的 window 对象或 document 对象,然后直接访问其 DOM 或 JavaScript 变量。

从 iframe 访问父页面

iframe 可以通过 window.parent 访问父页面的 window 对象。

示例

父页面(parent.html)

<!DOCTYPE html>
<html>
<head>
  <title>父页面</title>
</head>
<body>
  <h1>父页面</h1>
  <iframe id="myIframe" src="iframe.html"></iframe>
​
  <script>
    // 等待 iframe 加载完成
    document.getElementById('myIframe').onload = function() {
      // 获取 iframe 的 window 对象
      let iframeWindow = document.getElementById('myIframe').contentWindow;
​
      // 调用 iframe 中的函数
      iframeWindow.showMessageFromIframe();
​
      // 访问 iframe 中的变量
      console.log('从 iframe 获取的变量:', iframeWindow.iframeVariable);
​
      // 修改 iframe 中的 DOM 元素
      let iframeDocument = iframeWindow.document;
      iframeDocument.getElementById('iframeElement').innerText = '父页面修改了 iframe 的内容';
​
      // 从 iframe 获取输入框的值
      let inputValue = iframeDocument.getElementById('iframeInput').value;
      console.log('从 iframe 获取的输入框值:', inputValue);
    };
​
    // 定义供 iframe 调用的函数
    function showMessageFromParent() {
      alert('这是父页面的消息!');
    }
  </script>
</body>
</html>

iframe 页面(iframe.html)

<!DOCTYPE html>
<html>
<head>
  <title>iframe 页面</title>
</head>
<body>
  <h2>iframe 页面</h2>
  <p id="iframeElement">这是 iframe 中的段落。</p>
  <input type="text" id="iframeInput" value="iframe 输入框的默认值">
​
  <script>
    // 定义变量
    let iframeVariable = '这是 iframe 中的变量';
​
    // 定义函数
    function showMessageFromIframe() {
      alert('这是 iframe 中的消息!');
    }
​
    // 调用父页面的函数
    window.parent.showMessageFromParent();
​
    // 访问父页面的 DOM 元素
    let parentTitle = window.parent.document.title;
    console.log('父页面的标题是:', parentTitle);
​
    // 修改父页面的背景颜色
    window.parent.document.body.style.backgroundColor = '#f0f8ff';
  </script>
</body>
</html>

确保 iframe 已经加载完成,才能访问其内容。


跨域情况下的通信

当父页面和 iframe 来自不同的域名、协议或端口时,浏览器的同源策略将阻止它们直接访问对方的内容。

使用 postMessage 进行跨域通信

window.postMessage() 方法允许来自不同源的窗口之间安全地进行通信。

示例

父页面(parent.html)

<!DOCTYPE html>
<html>
<head>
  <title>父页面</title>
</head>
<body>
  <h1>父页面</h1>
  <iframe id="myIframe" src="https://aichihongdoudemao.com/iframe.html"></iframe>
​
  <script>
    let iframe = document.getElementById('myIframe');
​
    // 向 iframe 发送消息
    iframe.onload = function() {
      let message = { type: 'GREETING', text: '你好,iframe!' };
      iframe.contentWindow.postMessage(message, 'https://otherdomain.com');
    };
​
    // 接收来自 iframe 的消息
    window.addEventListener('message', function(event) {
      // 验证消息来源
      if (event.origin !== 'https://aichihongdoudemao.com/iframe.html') {
        console.warn('来自未知来源的消息:', event.origin);
        return;
      }
​
      // 处理消息
      console.log('收到来自 iframe 的消息:', event.data);
    });
  </script>
</body>
</html>

iframe 页面(iframe.html)

<!DOCTYPE html>
<html>
<head>
  <title>iframe 页面</title>
</head>
<body>
  <h2>iframe 页面</h2>
​
  <script>
    // 接收来自父页面的消息
    window.addEventListener('message', function(event) {
      // 验证消息来源
      if (event.origin !== 'https://aichihongdoudemao.com/iframe.html') {
        console.warn('来自未知来源的消息:', event.origin);
        return;
      }
​
      // 处理消息
      console.log('收到来自父页面的消息:', event.data);
​
      // 回复父页面
      let reply = { type: 'RESPONSE', text: '你好,父页面!' };
      event.source.postMessage(reply, event.origin);
    });
  </script>
</body>
</html>

在 postMessage 中指定准确的 targetOrigin,不要使用 "*",以防止消息被发送到不受信任的窗口。

最佳实践和安全考虑

明确的 targetOrigin:始终在 postMessage 中指定准确的目标源,防止数据泄露。

// 不推荐的做法
iframe.contentWindow.postMessage(message, '*'); // 可能导致安全风险
​
// 推荐的做法
iframe.contentWindow.postMessage(message, 'https://otherdomain.com');

验证 event.origin:在接收消息时,验证消息的来源。

window.addEventListener('message', function(event) {
  if (event.origin !== 'https://expected-origin.com') {
    console.warn('不可信的消息来源,已忽略。');
    return;
  }
  // 处理可信的消息
});

验证消息的数据结构和内容:确保消息的数据格式符合预期,防止被恶意代码利用。

if (typeof event.data !== 'object' || !event.data.type) {
  console.warn('消息格式不正确,已忽略。');
  return;
}

CORS 跨域原理

CORS 跨域的原理实际上是浏览器与服务器通过一些 HTTP 协议头来做一些约定和限制。可以查看 HTTP-访问控制(CORS)

IE 上跨域访问没有权限

在跨域发送 ajax 请求时提示没有权限。 因为IE浏览器默认对跨域访问有限制。需要在浏览器设置中去除限制。 方法: 设置 > Internet 选项 > 安全 > 自定义级别 > 在设置中找到其他 - 在【其他】中将【通过域访问数据源】启用。

如何实现 CORS?


客户端处理


客户端可以向远程服务器发送签名请求。

如下示例代码:在 CORS 请求中以 Authorization 标头的形式发送凭据:

function sendAuthRequestToCrossOrigin() {       var xhr = new XMLHttpRequest();       xhr.onreadystatechange = function() {           if (this.readyState == 4 && this.status == 200) {             document.getElementById("demo").innerHTML = this.responseText;           }       };       xhr.open('GET', "https://yuanjava:8000/categories", true);       xhr.setRequestHeader('Authorization', 'Bearer rtikkjhgffw456tfdd');       xhr.withCredentials = true;       xhr.send();   }   


服务器端处理


方法1:直接采用 SpringBoot 的注解 @CrossOrigin

如下示例代码如下,可以把 @CrossOrigin 加在每个 Controller 上,也可以加在它们的公共父类上:

@CrossOrigin   @RestController   public class TestController extends BaseController {          //  其他逻辑   }   

方法2: 采用过滤器(filter)的方式

如下示例代码:增加一个 CORSFilter 类,并实现 Filter 接口即可。

 `@Component   public class CORSFilter implements Filter {           @Override       public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)               throws IOException, ServletException {           HttpServletResponse res = (HttpServletResponse) response;           res.addHeader("Access-Control-Allow-Credentials", "true");           res.addHeader("Access-Control-Allow-Origin", "*");           res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");           res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");           if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {               response.getWriter().println("ok");               return;           }           chain.doFilter(request, response);       }       @Override       public void destroy() {       }       @Override       public void init(FilterConfig filterConfig) throws ServletException {       }   }`

方法3: 配置 Configuration

如下示例代码:增加一个配置类继承 WebMvcConfigurerAdapter 或者实现 WebMvcConfigurer 接口,项目启动时,会自动读取配置。

import org.springframework.context.annotation.Configuration;   import org.springframework.web.servlet.config.annotation.CorsRegistry;   import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;   @Configuration   public class CorsConfig extends WebMvcConfigurerAdapter {       static final String ORIGINS[] = new String[]{"GET", "POST", "PUT", "DELETE"};          @Override       public void addCorsMappings(CorsRegistry registry) {           registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods(ORIGINS).maxAge(3600);       }   }   

另外,在服务器,可以通过设置响应头部来细粒度配置 CORS,具体的如下:

1.允许所有源访问

HTTP/1.1 200 OK   Access-Control-Allow-Origin: *   

2.允许特定源访问

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   

3.允许凭据请求访问

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   Access-Control-Allow-Credentials: true   

4.允许特定方法和头部

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   Access-Control-Allow-Methods: GET, POST, PUT, DELETE   Access-Control-Allow-Headers: Content-Type, Authorization   

5.设置预检请求的缓存时间

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   Access-Control-Allow-Methods: GET, POST, PUT, DELETE   Access-Control-Allow-Headers: Content-Type, Authorization   Access-Control-Max-Age: 3600  // 3600秒   

通常来说,在服务器解决 CORS是一种比较常见和彻底的方式,我们可以在服务器灵活的设置允许跨域访问的域名或者地址。

7. 常见问题及解决方案
问题1:No ‘Access-Control-Allow-Origin’ header is present on the requested resource

问题描述:当浏览器发起跨域请求时,未在响应中找到 Access-Control-Allow-Origin 头部。

解决方案:确保服务器端正确设置了 Access-Control-Allow-Origin 头部。例如:

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   

问题2:The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’

问题描述:当请求包含凭据时,Access-Control-Allow-Origin 头部不能设置为通配符 *。

解决方案:明确指定允许的源,并确保设置了 Access-Control-Allow-Credentials 头部。例如:

HTTP/1.1 200 OK   Access-Control-Allow-Origin: https://yuanjava.com   Access-Control-Allow-Credentials: true   

问题3:CORS preflight channel did not succeed

问题描述:预检请求失败,可能是由于服务器未正确处理 OPTIONS 请求。

解决方案:确保服务器正确处理 OPTIONS 请求并返回相应的 CORS 头部。例如,在 Node.js/Express 中:

app.options('/api/data', (req, res) => {     res.header('Access-Control-Allow-Origin', 'https://yuanjava.com');     res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');     res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');     res.header('Access-Control-Allow-Credentials', 'true');     res.sendStatus(204);   });   

8. 总结
CORS 是现代 Web 开发中不可或缺的机制,它允许 Web 应用在安全的前提下进行跨域资源请求,通过理解 CORS 的工作原理和配置方法,可以帮助我们有效地解决跨域请求的问题。

JSONP 跨域实现

  • JSONP:出现的早,兼容性好(兼容低版本IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持 GET 请求,不支持 POST 请求。
  • CORS:出现的较晚,它是 W3C 标准,属于跨域 AJAX 请求的根本解决方案。支持 GET 和 POST 请求。缺点是不兼容某些低版本的浏览器。
  • Nginx反向代理:同源策略对服务器不加限制,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

二、JSONP 概述

JSONP (JSON with Padding) 是 JSON 的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。

2.1 JSONP原理

事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。

2.2 优点

  • 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制
  • 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequestActiveX的支持
  • 并且在请求完毕后可以通过调用callback的方式回传结果

2.3 缺点

  • 只支持GET请求而不支持 POST 等其它类型的 HTTP 请求
  • 它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript 调用的问题

三、JSONP 应用流程

  1. 设定一个script标签

    <script src="http://jsonp.js?callback=cb"></script>
    
    // 或
    
    let script = document.createElement('script');
    script.src = "http://jsonp.js?callback=cb";
    body.append(script)
  2. callback定义了一个函数名,而远程服务端通过调用指定的函数并传入参数来实现传递参数,将function(response)传递回客户端
router.get('/', function (req, res, next) {
    (() => {
        const data = {
            x: 10
        };
        let params = req.query;
        if (params.callback) {
            let callback = params.callback;
            console.log(params.callback);
            res.send(`${callback}(${JSON.stringify(data.x)})`);
        } else {
            res.send('err');
        }
    })();
});
  1. 客户端接收到返回的 JS 脚本,开始解析和执行function(response)

四、JSONP 实现

3.1 简单的实例:

一个简单的 JSONP 实现,其实就是拼接URL,然后将动态添加一个script元素到头部。

前端 JSONP 方法示例:

function jsonp(req) {
    var script = document.createElement('script');
    var url = req.url + '?callback=' + req.callback.name;
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}

前端 JS 示例:

function hello(res){
    alert('hello ' + res.data);
}
jsonp({
    url : '',
    callback : hello 
});

服务器端代码:

var http = require('http');
var urllib = require('url');

var port = 8080;
var data = {'data':'world'};

http.createServer(function(req,res){
    var params = urllib.parse(req.url,true);
    if(params.query.callback){
        console.log(params.query.callback);
        // jsonp
        var str = params.query.callback + '(' + JSON.stringify(data) + ')';
        res.end(str);
    } else {
        res.end();
    }
    
}).listen(port,function(){
    console.log('jsonp server is on');
});

3.2 可靠的 JSONP 实例:

(function (global) {
    var id = 0,
        container = document.getElementsByTagName("head")[0];

    function jsonp(options) {
        if(!options || !options.url) return;

        var scriptNode = document.createElement("script"),
            data = options.data || {},
            url = options.url,
            callback = options.callback,
            fnName = "jsonp" + id++;

        // 添加回调函数
        data["callback"] = fnName;

        // 拼接url
        var params = [];
        for (var key in data) {
            params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
        }
        url = url.indexOf("?") > 0 ? (url + "&") : (url + "?");
        url += params.join("&");
        scriptNode.src = url;

        // 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法
        global[fnName] = function (ret) {
            callback && callback(ret);
            container.removeChild(scriptNode);
            delete global[fnName];
        }

        // 出错处理
        scriptNode.onerror = function () {
            callback && callback({error:"error"});
            container.removeChild(scriptNode);
            global[fnName] && delete global[fnName];
        }

        scriptNode.type = "text/javascript";
        container.appendChild(scriptNode)
    }

    global.jsonp = jsonp;

})(this);

使用示例:

jsonp({
    url : "www.example.com",
    data : {id : 1},
    callback : function (ret) {
        console.log(ret);
    }
});

五、JSONP安全性问题

5.1 CSRF攻击

前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。

解决方法:验证JSONP的调用来源(Referer),服务端判断 Referer 是否是白名单,或者部署随机 Token 来防御。

5.2 XSS漏洞

不严谨的 content-type 导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://abc.com?callback=douniwan, 然后返回 douniwan({ data }),那假如请求 http://abc.com?callback=<script>alert(1)</script> 不就返回 <script>alert(1)</script>({ data })了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback 参数,直接当 HTML 解析了,就是一个赤裸裸的 XSS 了。

解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成&lt>换成&gt)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。

5.3 服务器被黑,返回一串恶意执行的代码

可以将执行的代码转发到服务端进行校验 JSONP 内容校验,再返回校验结果。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值