什么是跨域以及解决方案

同源策略

同源策略限制了不同源之间如何进行资源交互,是用于隔离潜在恶意文件的重要安全机制。 同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。 如果没有同源策略,不同源的数据和资源(如HTTP头、Cookie、DOM、localStorage等)就能相互随意访问,根本没有隐私和安全可言。为了安全起见和资源的有效管理,浏览器当然要采用这种策略。 什么是同源:如果两个 URL 的协议,域名和端口都相同的话,则这两个 URL 是同源。

什么是同源?举例来说,www.example.com/dir/page.ht…

如何允许跨源访问?

跨域资源共享(CORS:Cross-Origin Resource Sharing)

CORS是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。 CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 XMLHttpRequestXMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。所以实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

简单请求

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

  1. 请求方法是以下三种方法之一:HEADGETPOST
  2. HTTP的头信息不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

Last-Event-ID服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events。 通过Server-Sent Events可以从服务器向浏览器推送信息,Last-Event-ID是当连接断了之后,重新连接后,发送Last-Event-ID,以便服务器可以重新发送后面的消息。 详细内容:Server-Sent Events 教程

对于简单请求,就是在头信息之中,增加一个Origin字段。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指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应(状态码:200)。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了。

Access-Control-Allow-Origin: *
复制代码

Access-Control-Allow-Origin:服务器端配置的支持的Origin的值,不一定是请求的Origin。

非简单请求

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, 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方法。
  2. Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会发送的头信息字段。

预检请求的回应 服务器收到"预检"请求以后,检查了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
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
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字段,表示api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。 如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
复制代码
  1. Access-Control-Allow-Methods

它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。

  1. Access-Control-Allow-Headers

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

  1. Access-Control-Allow-Credentials

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

  1. Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。 一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

Spring对CORS的支持

自定义Filter

@Component
public class CORSFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       HttpServletResponse response = (HttpServletResponse) servletResponse;
       response.setHeader("Access-Control-Allow-Origin", "*");
       response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
       response.setHeader("Access-Control-Max-Age", "3600");
       response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
       response.setHeader("Access-Control-Allow-Credentials", "true");
       filterChain.doFilter(servletRequest, servletResponse);
  }

  @Override
  public void destroy() {
  }
}
复制代码

CorsFilter

since:Spring MVC 4.2

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
 
 
@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
 
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}
复制代码

@CrossOrigin 注解:作用于方法或类上

  1. value,origins:允许来源域名的列表,默认支持所有的origins
  2. originPatterns:https://*.domain.com
  3. allowedHeaders:允许的请求头中的字段类型,默认支持所有的header字段(Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma)跨域访问。
  4. methods:跨域HTTP请求中支持的HTTP请求类型,不指定确切值时默认与 Controller 方法中的 methods 字段保持一致。
  5. allowCredentials:Access-Control-Allow-Credentials,浏览器是否将本域名下的 cookie 信息携带至跨域服务器中。默认携带至跨域服务器中,但要实现 cookie 共享还需要前端在 AJAX 请求中打开 withCredentials 属性,如果使用fetch,则credentials设置为include

Fetch中解决跨域携带cookies问题credentials的值可能为 omitsame-origin 或者 include

  • omit:缺省值, 默认为该值,不发送cookie
  • same-origin:表示同域请求才发送cookie
  • include:发送cookie
  1. maxAge:Access-Control-Max-Age,预检请求响应的缓存持续的最大时间,目的是减少浏览器预检请求/响应交互的数量。默认值30分钟。设置了该值后,浏览器将在设置值的时间段内对该跨域请求不再发起预请求。

WebMvcConfigurer

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

   @Override
   public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://domain2.com")
                .allowedMethods("PUT", "DELETE")
                    .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(false).maxAge(3600);
   }
}
复制代码

WebMvcConfigurerAdapter 是一个实现了WebMvcConfigurer 接口的抽象类,并提供了全部方法的空实现,我们可以在其子类中覆盖这些方法,以实现我们自己的配置,如视图解析器,拦截器和跨域支持等。 但是,从Spring 5开始,WebMvcConfigure接口包含了WebMvcConfigurerAdapter类中所有方法的默认实现,因此WebMvcConfigurerAdapter这个适配器就被标记为@Deprecated,我们可以直接将原来的继承WebMvcConfigurerAdapter类改为实现WebMvcConfigurer接口,其余的地方都没有变化。

原理分析

CorsFilter

CorsFilter实现CorsFilter接口,在DispatcherServlet处理请求之前进行拦截。

AbstractHandlerMethodMapping

AbstractHandlerMethodMapping实现了InitializingBean接口,包括afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。

DefaultCorsProcessor

实现CorsProcessor接口,Spring 中对 CORS 规则的校验,都是通过委托给 DefaultCorsProcessor实现的。 DefaultCorsProcessor 处理过程如下:

  1. 判断依据是 Header中是否包含 Origin。如果包含则说明为 CORS请求,转到 2;否则,说明不是 CORS 请求,不作任何处理。
  2. 判断 response 的 Header 是否已经包含 Access-Control-Allow-Origin,如果包含,证明已经被处理过了, 转到 3,否则不再处理。
  3. 判断是否同源,如果是则转交给负责该请求的类处理。
  4. 是否配置了 CORS 规则,如果没有配置,且是预检请求,则拒绝该请求,如果没有配置,且不是预检请求,则交给负责该请求的类处理。如果配置了,则对该请求进行校验。

校验就是根据 CorsConfiguration 这个类的配置进行判断:

  1. 判断 origin 是否合法。
  2. 判断 method 是否合法。
  3. 判断 header是否合法。
  4. 如果全部合法,则在 response header中添加响应的字段,并交给负责该请求的类处理,如果不合法,则拒绝该请求。

JSONP跨域

它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。 缺点是仅支持GET请求。 首先,网页动态插入<script>元素,由它向跨源网址发出请求。

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('Your public IP address is: ' + data.ip);
};
复制代码

上面代码通过动态添加<script>元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。 服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

Nginx配置

server {
    location /api {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Headers' '*';
      add_header 'Access-Control-Allow-Methods' '*';
  }
}
复制代码

参考

  1. 跨域资源共享 CORS 详解
  2. Nginx跨域配置
  3. Spring 注解面面通 之 @CrossOrigin 注册处理方法源码解析
  4. 浅探SpringMVC中HandlerExecutionChain之handler、interceptor

猜你喜欢

转载自juejin.im/post/7132043503411920926