4

深入解析各种解决跨域的方法

同源策略

同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略来对脚本和请求进行校验,若不同源,则禁止使用。

同源的定义

那如果判断是否同源?主要根据三个维度,域名,协议,端口三个都相同才算同源。
举个?:

网站A 网站B 结果
http://www.zhenai.com http://i.zhenai.com 不同源,域名不同
http://www.zhenai.com http://www.zhenai.cn 不同源,域名不同
http://www.zhenai.com https://www.zhenai.com 不同源,协议不同
http://www.zhenai.com http://www.zhenai.com:3000 不同源,端口不同(默认端口80)

同源策略的作用

①无法用js读取非同源的Cookie、LocalStorage 和 IndexDB

这个主要是为了防止恶意网站通过js获取用户其他网站的cookie等用户信息。

②无法用js获取非同源的DOM

防止恶意网站通过iframe获取页面dom,从而窃取页面的信息。

③无法用js发送非同源的AJAX请求

防止恶意的请求攻击服务器窃取数据信息。

那是不是说非同源的请求就无法实现呢?也不是,这就引出了我们本文主要阐述的解决跨域请求问题的方法。

jsonp

jsonp能实现跨域是利用了img、script和link标签自身的跨域能力。
我们知道当img或者script中的src是一个链接的时候,浏览器会请求这个链接获取资源,那么这个链接如果是跨域的,浏览器也会请求,从而达到了跨域请求的一个功能。

用法

var script = document.createElement('script');
script.src = 'http://localhost:3000/api/test.do?a=1&b=2&callback=cb';
$('body').append(script);

function cb(res){
    // do something
    console.log(res)
}

可以看到,我们创建一个script标签,将src改成我们要请求的接口,并将script添加在body中,那么当浏览器解析到这个script时,会想src对应的服务器发送一个get请求,并将参数带过去。
然后当浏览器接收到服务端返回的数据,就会触发参数中callbak对应的回调函数cb,从而完成整个get请求。

优点

简单粗暴

缺点

①只支持get请求
②需要后台配合,将返回结果包装成callback(res)的形式

防范

那如果黑客植入script脚本通过jsonp的方式对服务器进行攻击,怎么办?
可以通过页面设置的内容安全协议csp进行防范。

cors跨域

cors 是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),它允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制
cors 需要浏览器和服务器同时支持,整个 CORS通信过程,都是浏览器自动完成不需要用户参与,对于开发者来说,cors的代码和正常的 ajax 没有什么差别,浏览器一旦发现跨域请求,就会添加一些附加的头信息
但是,cors不支持ie10及以下版本。

简单请求和复杂请求

浏览器将cors请求分为简单请求和复杂请求。
简单请求则直接发送请求到服务器,只请求一次。
而复杂请求在正式请求前都会有预检请求,在浏览器中都能看到有OPTIONS请求,用于向服务器请求权限信息的,需要请求两次。

那如何区分是简单请求还是复杂请求呢?

简单请求

简单请求必须要同时满足下面三个条件:

  1. 请求方式只能是:GET、POST、HEAD
  2. HTTP请求头限制这几种字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
  3. Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
content-type的类型
类型 描述
application/json 消息主体是序列化后的 JSON 字符串
application/x-www-form-urlencoded 数据被编码为键值对。这是标准的编码格式
multipart/form-data 需要在表单中进行文件上传时,就需要使用该格式。常见的媒体格式是上传文件之时使用的
text/plain 数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符

application/json:

  • 作用: 告诉服务器请求的主题内容是json格式的字符串,服务器端会对json字符串进行解析,
  • 好处: 前端人员不需要关心数据结构的复杂度,只要是标准的json格式就能提交成功。

application/x-www-form-urlencoded:是Jquery的Ajax请求默认方式

  • 作用:在请求发送过程中会对数据进行序列化处理,以键值对形式?key1=value1&key2=value2的方式发送到服务器。
  • 好处: 所有浏览器都支持。

复杂请求

不满足简单请求的条件,那么就是复杂请求。
复杂请求会在正式请求发送之前,先发一个预检请求进行校验,校验通过后才能进行正式请求。
举个?
浏览器现在要发送一个put的复杂请求,那么在put请求发送之前,浏览器先发送一个options请求。
options请求头信息:

OPTIONS /cors HTTP/1.1
Origin: localhost:3000
Access-Control-Request-Method: PUT // 表示使用的什么HTTP请求方法
Access-Control-Request-Headers: X-Custom-Header // 表示浏览器发送的自定义字段
Host: localhost:3000
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器收到options请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应
options响应头信息

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://localhost:3000 // 表示http://localhost:3000可以访问数据
Access-Control-Allow-Methods: GET, POST, PUT      
Access-Control-Allow-Headers: X-Custom-Header    
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

当options请求通过之后发出正式的HTTP请求,倘若options请求不通过,则服务器不允许此次访问,从而抛出错误

options请求通过之后的,浏览器发出发请求

PUT /cors HTTP/1.1
Origin: http://api.zhenai.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
options请求缓存

那这样的话,如果页面存在大量的复杂请求,岂不是每个请求前面都要进行一次options的请求,那不会造成大量资源的浪费么?
如果基于cors请求的方法来解决跨域问题,那么复杂请求之前是需要进行一个options的请求的,但我们可以通过对options请求进行缓存来减轻请求的压力。

在options请求中,我们可以通过设置响应头的参数Access-Control-Max-Age来对结果进行缓存
比如: Access-Control-Max-Age: 600 表示对options检验结果进行十分钟的缓存

  1. url变化会导致缓存失效,需要重新验证options请求的返回值
  2. 预检不关心post data
  3. header变化,如果是去掉了自定义的header使得请求变成简单请求,不会发送options请求。如果是增加其他的header,是会重新验证Access-Control-Allow-Headers的值。
  4. cookie变化,只要后端允许发送cookie,cookie值变化不会导致缓存失效。

该字段的兼容性如下:
image.png

nginx

nginx解决跨域的问题跟之前的方法有所不同,它是通过服务器的方向代理,将前端访问域名跟后端服务域名映射到同源的地址下,从而实现前端服务和后端服务的同源,那自然不存在跨域的问题了。
举个?:
前端服务:http://localhost:3000
前端页面路由:http://localhost:3000/page.html
后端服务:http://localhost:3001
后端接口路由:http://localhost:3001/api/test.do
可以看出,两个服务处于跨域的状态
通过nginx的配置进行反向代理,即可实现前后端服务同源,如下:

server
{
    listen 80;
    server_name localhost;

    location = / {
        proxy_pass http://localhost:3000;
    }

   location /api {
        proxy_pass http://localhost:3001;

        #指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #$http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #OPTIONS预检命令,预检命令通过时才发送请求
        #检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
   }
}

其实nginx不仅仅只是用于解决跨域问题,而是涉及到很多服务器资源分配的处理,在此就不详细探讨了。

vue proxyTable

其实,在我们主流使用的MVVM框架中,配置项里面也提供解决跨域问题的能力,继续举个?,以vue2.x为例,我们可以通过在config/index.js中添加配置项实现跨域请求:

proxyTable: {
    '/apis': {
        // 测试环境
        target: 'http://www.zhenai.com/',  // 接口域名
        changeOrigin: true,  //是否跨域
        pathRewrite: {
            '^/apis': ''   //需要rewrite重写的,
        } 
    }             
}

原理

其实原理很简单,就是在我们使用npm run dev命中,启动了一个node服务,然后将前端发出的请求发送到node服务,再将该服务转发到原本的后台服务,在这过程中实现了一层代理,由一个node服务发送一个请求到另外一个后台服务,自然也没有了浏览器所限制的跨域问题。

参考文献

https://blog.csdn.net/yingwang9/article/details/90716623
https://www.jianshu.com/p/d89c62572acd
https://segmentfault.com/a/1190000019227927?utm_source=tag-newest


TheWalkingFat
522 声望32 粉丝