# 跨域

一个源的定义

如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。 举个例子:

下表给出了相对http://a.lanjz.com/dir/page.html同源检测的示例: 
URL                                         结果          原因
http://a.lanjz.com/dir2/other.html            成功     协议,端口(如果有指定)和域名都相同
http://a.lanjz.com/dir/inner/another.html     成功    协议,端口(如果有指定)和域名都相同 
https://a.lanjz.com/secure.html               失败    不同协议 ( https和http )
http://a.lanjz.com:81/dir/etc.html            失败    不同端口 ( 81和80)
http://a.opq.com/dir/other.html             失败    不同域名 ( lanjz和opq)

同源策略

同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源

# 跨域请求的解决方案

# 基于jsonp实现的跨域请求

跨域资源的引入是可以的,如嵌入到页面中的scriptimglinkiframe等。

jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据

例子

服务端:

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

var port = 10011;
var data = {'name': 'jifeng', 'company': 'taobao'};

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

前端JS

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.localhost:10011?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

JSONP的缺点:

  • 具有局限性, 仅支持get方法

  • 不安全,可能会遭受XSS攻击

# form表单提交

ajax跨域是因为浏览需要保护用户的隐私而给JS设定的限制,form表单提交会刷新页面,原页面的脚本无法获取新页面的内容,所以浏览器认为是安全的

<form action="http://www.localhost:10011">
    <button type="submit">提交</button>
</form>

# CORS(Cross-origin resource sharing)

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

服务器设置Access-Control-Allow-Origin:允许跨域的域名

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

服务器还有其它配置

  • 服务器设置Access-Control-Allow-Credentials

    正常跨域请求是不允许携带Cookie的,如果需要带Cookie还需要进行以下配置

    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下值为false,Cookie不包括在CORS请求之中 设为true后,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。

    服务器设置之后,前端请求还需要配置withCredentials属性

  • 服务器设置Access-Control-Expose-Headers

    CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。 上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值

# 修改document.domain

这个解决方案只适用于主域,协议,端口相同,子域不同的跨域应用场景。其他情况不能使用

比如在:aaa.com的一个网页a.html里面利用iframe引入了一个bbb.com里的一个网页b.html。 这时在a.html里面可以看到b.html里的内容,但是却不能利用javascript来操作它。因为这两个页面属于不同的域

如果在a.html里引入aaa.com里的另一个网页,是不会有这个问题的,因为域相等

有另一种情况,两个子域名:aaa.xxx.combbb.xxx.comaaa.xxx.com里的一个网页(a.html)引入了bbb.xxx.com 里的一个网页 b.html,这时a.html里同样是不能操作b.html里面的内容的。因为document.domain不一样(一个是aaa.xxx.com,另一个是bbb.xxx.com

这时我们就可以通过Javascript将两个页面的domain改成一样的,需要在a.html里与b.html里都加入代码如下:

document.domain = "xxx.com"

这样这两个页面就可以互相操作了。也就是实现了同一基础域名之间的"跨域"

# 利用服务器代理

跨域是浏览器给的限制,所以平时开发时可以利用node加一层代理转发接口的方式来解决跨域问题,Webpack也提供了相关的配置:

const domain = "http://i.chuangliang.com/"

devServer: {
    proxy: {
      "/api": {
        target: domain,
        ws: true,
        changeOrigin: true,
        cookieDomainRewrite: true,
        pathRewrite: {
          '^/api': '/'
        },
        onProxyReq: function onProxyReq(proxyReq) {
          console.warn(`代理请求===${proxyReq.path}`);
        }
      }
    }
  },

# HTTP响应首部字段

  • Accept-Control-Allow-Origin: 用于设置允许访问该资源的外部URL

  • Accept-Control-Expose-Headers:对跨域访问在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-ControlContent-LanguageContent-TypeExpiresLast-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。

Access-Control-Expose-Headers 头让服务器把允许浏览器访问的头放入白名单,例如:

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
  • Access-Control-Max-Age:指定了preflight请求的结果能够被缓存多久

  • Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器

  • Access-Control-Allow-Methods:首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

# HTTP请求头部字段

  • origin:发起请求的源站URL,不包含路径

    注意:无论是否跨域请求,origin都会被发送

  • Accept-Control-Request-Method:用于预检请求,将实际所请求的方法告诉服务器

    在项目中如果是跨域请求时,我们发现同一个请求有两次发送,第一次是options,第二次才是真正的请求。第一次请求就是预检请求,请求头中会带上Accept-Control-Request-Method,值是我们真正要请求的方法

# 预检请求

对于非简单请求如POST,PUTDELETE,在发这些类型的请求之前会先发送一个"预检"请求, 请求方法是OPTIONS

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。 只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

一般预检请求都包含以下几个头部:

  • Origin: 表示请求来自哪个源

  • Access-Control-Request-Method: 用来列出浏览器的CORS请求会用到哪些HTTP方法

  • Access-Control-Request-Headers: 浏览器CORS请求会额外发送的头信息字段

服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有 Access-Control-Allow-Origin 这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。

服务器回应的CORS的字段如下:

Access-Control-Allow-Origin: http://api.bob.com  // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header  // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true   // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000  // 用来指定本次预检请求的有效期,单位为秒

# 减少OPTIONS请求次数:

OPTIONS请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少 OPTIONS 请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。

跨域资源共享 CORS 详解 (opens new window)

# Q&A

# 正向代理和反向代理的区别

正向代理:

客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。

比如翻墙工具

反向代理:

服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。

两者区别如图示:

代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。