Fork me on Github

js跨域及解决方案

 原文链接:https://github.com/FIGHTING-TOP/FE-knowlodge-base

什么是跨域请求

当前发起请求的域与该请求指向的资源(该请求所在的页面)所在的域不一样。

此时该请求就会受到浏览器SOP(同源策略)的限制,这样做可以降低被CSRF(跨站请求伪造)攻击的可能,但是同源策略并不能避免CSRF。对CSRF感兴趣的可以戳这里

这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。

 

如何解决ajax跨域

  • JSONP
  • CORS
  • 代理请求
  • 图片Ping
  • websocket

 

(一)JSONP方式解决跨域问题

jsonp解决跨域问题是一个比较古老的方案(实际中不推荐使用),这里做简单介绍(实际项目中如果要使用JSONP,一般会使用JQ等对JSONP进行了封装的类库来进行ajax请求)

实现原理

JSONP之所以能够用来解决跨域方案,主要是因为 <script> 脚本拥有跨域能力,而JSONP正是利用这一点来实现。

当我们用一个script的src来请求一个接口时,这个接口返回了一个函数名和一个或一些参数,由于是script引入的,所以浏览器就直接给我们执行了,注意在这之前我们必须先注册好接口返回的方法。

实现流程

JSONP的实现步骤大致如下(参考了来源中的文章)

  • 客户端网页网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制

    function addScriptTag(src) {
      var script = document.createElement('script');
      script.setAttribute("type","text/javascript");
      script.src = src;
      document.body.appendChild(script);
    }
    
    window.onload = function () {
      addScriptTag('http://example.com/ip?callback=foo');
    }
    
    function foo(data) {
      console.log('response data: ' + JSON.stringify(data));
    };     
    

    请求时,接口地址是作为构建出的脚本标签的src的,这样,当脚本标签构建出来时,最终的src是接口返回的内容

  • 服务端对应的接口在返回参数外面添加函数包裹层
foo({
  "test": "testData"
});  

  

  • 由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。

注意,一般的JSONP接口和普通接口返回数据是有区别的,所以接口如果要做JSONP兼容,需要进行判断是否有对应callback关键字参数,如果有则是JSONP请求,返回JSONP数据,否则返回普通数据

使用注意

基于JSONP的实现原理,所以JSONP只能是“GET”请求,不能进行较为复杂的POST和其它请求,所以遇到那种情况,就得参考下面的CORS解决跨域了

 

(二)CORS解决跨域问题

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

同源安全策略 默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。

 

他允许我们设置一些请求头信息来解决跨域:

Access-Control-Allow-Origin // 指示请求的资源能共享给哪些域。

Access-Control-Allow-Credentials // 指示当请求的凭证标记为 true 时,是否响应该请求。

Access-Control-Allow-Headers // 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。

Access-Control-Allow-Methods // 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。

Access-Control-Expose-Headers // 指示哪些 HTTP 头的名称能在响应中列出。

Access-Control-Max-Age // 指示预请求的结果能被缓存多久。

Access-Control-Request-Headers // 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。

Access-Control-Request-Method // 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。

Origin // 指示获取资源的请求是从什么域发起的。

  

另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求

在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

然而并非所有的请求都会有预检请求,详细资料可以阅读MDN文档介绍的 跨源资源共享(CORS)

 

(三)代理请求方式解决接口跨域问题

注意,由于接口代理是有代价的,所以这个一般仅是开发过程中使用。

与前面的方法不同,前面CORS是后端解决,而这个主要是前端对接口进行代理,也就是:

  • 前端ajax请求的是本地接口
  • 本地接口接收到请求后向实际的接口请求数据,然后再将信息返回给前端
  • 一般用node.js即可代理

关于如何实现代理,方法有很多,那么我们开发一般会用webpack-dev-server

假如我们服务在 localhost:3000 上,你可以这样启用代理:

webpack.config.js

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: {'^/' : ''}
      }
    }
  }
};

  

请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users

如果你不想始终传递 /api ,则需要重写路径:

webpack.config.js

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: {'^/api' : ''}
      }
    }
  }
};

  

这种代理的方式呢,其实是用http-proxy-middleware来实现的, 有兴趣的可以看下源码了解下,有机会可以专门聊下这个

 

 

(四)图片Ping

我们知道,一个网页可以从任何网页加载图像,不用担心跨域不跨域,所以,我们就可以利用图片不受“同源限制”这一点进行跨域通信。 

我们利用JS创建一个新的Image对象,并把src属性设置为指向请求的地址,通过监听onload和onerror事件来确定是否接受到了响应。响应的数据可以是任意内容,但通常是像素图或304响应。

需要注意的是,新图像元素只要设置了src属性就会开始下载,所以我们这里的事件一定要在指定src属性之前绑定,这也是为什么我们这里不需要把img标签插入DOM 的原因。

<body>
    <button id="Ping">图像Ping发送请求</button>
    <script>
        var btn=document.getElementById('Ping');
        btn.onclick=function () {
            var img=new Image();
            img.onload=img.onerror=function () {
                alert("Done");
            };
            img.src="http://localhost:8081/img?name=Joy";
        }
    </script>
</body>
//服务器代码
app.get('/img',function (req,res) {
   res.send("我是一张图片");
});

  

 

这种方式优点是很明显的:兼容性非常好,缺点就是:只能发生GET请求,而且无法获取响应文本。

 

(五)WebSocket

 WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

它默认是可以跨域的。

// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});

  

 

对于前端页面之间的跨域

  • 如果是iframe可以使用 location.hash 或 window.name 进行信息交流
  • 使用 postMessage
  • document.domain(仅限于一级域名和二级域名之间)

Reference:

 

posted @ 2018-01-20 11:15  王者归来!  阅读(274)  评论(0编辑  收藏  举报