跨域的本质与解决方案

简介

       相信你们在作web系统开发的时候,都遇到过前端页面访问另外一个服务器而非本服务器从而遭遇的跨域问题。跨域是个很常见的问题,虽然在web系统中的解决方式很简单,加一段耳熟能详的代码,或者一个注解,或者在某个框架中配置点什么就能解决,可是很多开发者并无对跨域有全面的系统性的认知。javascript

​​​​​原文做者:千里明月(开源中国)

原文连接:https://my.oschina.net/u/3490860/blog/write  

参考文章:http://www.ruanyifeng.com/blog/2016/04/cors.htmlhttp://www.javashuo.com/article/p-zmslogqt-hb.html

1、何为跨域

1.  浏览器的同源策略Same-origin Policy

      若是两个url的协议、域名或ip、端口都相同,即在浏览器中被视为二者同源。下表给出了相对http://store.company.com/dir/page.html同源检测的示例:html

URL 结果 缘由
http://store.company.com/dir2/other.html 成功  
http://store.company.com/dir/inner/another.html 成功  
https://store.company.com/secure.html 失败 不一样协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不一样端口 ( 81和80)
http://news.company.com/dir/other.html 失败 不一样域名 ( news和store )
  • DOM同源策略:禁止对不一样源的页面的DOM进行操做,主要包括iframe、canvas之类的。不一样源的iframe禁止数据交互的,含有不一样源数据的canvas会受到污染而没法进行操做。
  • XmlHttpRequest同源策略:禁止不一样源的AJAX请求,主要用来防止CSRF攻击

2.  CORS

      CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它容许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。也就是说CORS是为了解决跨源访问问题的一套机制。前端

      CORS须要浏览器和服务器同时支持。目前,全部浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通讯过程,都是浏览器自动完成,不须要用户参与。对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。所以,实现CORS通讯的关键是服务器。只要服务器实现了CORS接口,就能够跨源通讯。java

2.  跨域的表现

      设有两个服务器A和B,A的页面调用A的接口,没有问题;可是A的页面调用B的接口,将会产生跨域问题。web

进入A的index页面spring

 

 

两个按钮的对应的触发代码json

function testA(){
        $.get("/testA", function(data){
            alert("Data Loaded: " + data);
        });
    }
    function testB(){
        $.get("localhost:8081/testB", function(data){
            alert("Data Loaded: " + data);
        });
    }

点击左边按钮,对应后台代码为:canvas

@GetMapping("/testA")
    public String test(){
        System.out.println("访问A网站");
        return "访问A网站";
    }

这时触发效果:segmentfault

访问正常,可是当点击右边按钮时,就会报错api

大致意思就是从源localhost:8080发出的请求localhost:8081/testB被CORS跨资源共享机制阻止了。由于端口不一样。

 

3.  跨域引发的问题

    那么为何会防范跨域这个机制呢,由于若是容许跨域会发生诸多的安全问题。如CSRF攻击:

一个网站用户Bob可能正在浏览聊天论坛,而同时另外一个用户Alice也在此论坛中,而且后者刚刚发布了一个具备Bob银行连接的图片消息。设想一下,Alice编写了一个在Bob的银行站点上进行取款的form提交的连接,并将此连接做为图片src。若是Bob的银行在cookie中保存他的受权信息,而且此cookie没有过时,那么当Bob的浏览器尝试装载图片时将提交这个取款form和他的cookie,这样在没经Bob赞成的状况下便受权了此次事务。

2、跨域的及解决方案

1. 根本上须要从http协议自己入手

CORS须要浏览器和服务器同时支持。目前,全部浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通讯过程,都是浏览器自动完成,不须要用户参与。对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。若是须要传递cookie信息,则须要前端额外设置一些信息。

1.1 浏览器将CORS请求分红两类:

简单请求(simple request)和非简单请求(not-so-simple request)

只要同时知足如下两大条件,就属于简单请求。

(1) 请求方法是如下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出如下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不一样时知足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不同的。

1.2 简单请求

 对于简单请求,浏览器直接发出CORS请求。具体来讲,就是在头信息之中,增长一个Origin字段。

下面是一个例子,浏览器发现此次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来讲明,本次请求来自哪一个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否赞成此次请求。

若是Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误没法经过状态码识别,由于HTTP回应的状态码有多是200。

若是Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

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

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求, 可是若是要传递cookie,那么就不能设置*。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否容许发送Cookie。默认状况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器。这个值也只能设为true,若是服务器不要浏览器发送Cookie,删除该字段便可。

(3)Access-Control-Expose-Headers

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

withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。若是要把Cookie发到服务器,一方面要服务器赞成,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另外一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

不然,即便服务器赞成发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

可是,若是省略withCredentials设置,有的浏览器仍是会一块儿发送Cookie。这时,能够显式关闭withCredentials

xhr.withCredentials = false;

须要注意的是,若是要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其余域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也没法读取服务器域名下的Cookie。

1.3 非简单请求

非简单请求是那种对服务器有特殊要求的请求,好比请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求(preflight)。

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

下面是一段浏览器的JavaScript脚本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,而且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确承认以这样请求。下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪一个源。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

 

服务器收到"预检"请求之后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段之后,确认容许跨源请求,就能够作出回应。

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://api.bob.com
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

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com能够请求数据。该字段也能够设为星号,表示赞成任意跨源请求。

Access-Control-Allow-Origin: *

若是浏览器否认了"预检"请求,会返回一个正常的HTTP回应,可是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不一样意预检请求,所以触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出以下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其余CORS相关字段以下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,代表服务器支持的全部跨域请求的方法。注意,返回的是全部支持的方法,而不单是浏览器请求的那个方法。这是为了不屡次"预检"请求。

(2)Access-Control-Allow-Headers

若是浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,代表服务器支持的全部头信息字段,不限于浏览器在"预检"中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即容许缓存该条回应1728000秒(即20天),在此期间,不用发出另外一条预检请求。

1.3.1 浏览器的正常请求和回应

一旦服务器经过了"预检"请求,之后每次浏览器正常的CORS请求,就都跟简单请求同样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是"预检"请求以后,浏览器的正常CORS请求。

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

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

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

上面头信息中,Access-Control-Allow-Origin字段是每次回应都一定包含的。

2. 具体方案

2.1 针对springboot

@Configuration
public class MyConfiguration {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**");
            }
        };
    }
}

Spring Framework还提供了CorsFilter。在这种状况下,能够在Spring Boot应用程序中声明过滤器:

@Configuration
public class MyConfiguration {

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://domain1.com");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}
相关文章
相关标签/搜索