简介

​ CORS(Cross-Origin Resource Sharing 跨源资源共享),当一个资源请求的域或端口与该资源所在服务器的域或端口不同时即为跨域。

例如,在网页 lombok使用 中,图片引用却是另一个域名的资源。

资源跨域

​ 出于安全考虑,浏览器限制从脚本内发起的跨源 HTTP 请求,例如,XMLHttpRequestFetch API 遵循同源策略。 这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件。

​ W3C 推荐了一种跨域的访问验证的机制,即 CORS(Cross-Origin Resource Sharing 跨源资源共享)。CORS允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。

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

​ 整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

流程

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

若请求满足所有下述条件,则该请求可视为“简单请求”:

  • 使用以下请求方法之一的

    • GET
    • HEAD
    • POST
  • HTTP头信息不超出以下集合的:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限于:text/plain, multipart/form-data, application/x-www-form-urlencoded)

简单请求

浏览器发出 CORS 请求,在头信息中增加 Origin 字段,流程如下图:

简单请求流程

以下是使用 axios 封装的请求函数,以下内容发送测试的请求均使用此函数——fetch:

const fetch = (type = 'GET', url, param = '') => {
  return new Promise((resolve, reject) => {
    axios({
      method: type,
      url: url,
      changeOrigin: true,
      data: JSON.stringify(param)
    }).then((response) => {
      resolve(response)
    }, err => {
      reject(err)
    }).catch((error) => {
      reject(error)
    })
  })
}

测试发送js:

fetch('GET', 'http://127.0.0.1:8090/test').then((response) => {
    console.log(response.data)
}).catch((err) => {
    console.log(err)
})

发送测试请求报文和响应报文:

// 请求报文

GET http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Referer: http://127.0.0.1:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7

// 响应报文

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Content-Type: application/json;charset=UTF-8
Date: Mon, 25 Jun 2018 04:09:32 GMT
Content-Length: 51

{"status":true,"code":200,"message":"操作成功"}

请求报文的字段 Origin 表示该请求源于 http://127.0.0.1:8080
响应报文中的字段 Access-Control-Allow-Origin 值为 * ,说明该资源可以被任意外域访问。
如果 Access-Control-Allow-Origin 值不为 *http://127.0.0.1:8080 ,那么浏览器会抛出错误,如下图所示,此时要做的就是跨域处理了。

跨域错误

使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制,若要访问该资源,Access-Control-Allow-Origin 应当为 * 或者是 Origin 字段所指明的域名。

非简单请求

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

非简单请求的 CORS 请求会先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。只有收到肯定答复,浏览器才会发出正式的请求,流程如下图:

预检请求流程图

测试发送js:

fetch('PUT', 'http://127.0.0.1:8090/test').then((response) => {
    console.log(response.data)
}).catch((err) => {
    console.log(err)
})

浏览器发现这是一个非简单请求,会先发送一个预检请求,请求服务器确认是否允许该请求,以下是请求报文:

OPTIONS http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: PUT
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Access-Control-Request-Headers: authorization
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7

预检请求用的请求方法是 OPTIONS,字段 Origin 表示请求来自哪个源。

服务器收到预检请求后,验证了 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 字段以后,确认允许该跨域请求,响应报文:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Content-Length: 0
Date: Mon, 25 Jun 2018 06:50:11 GMT

响应报文中的字段 Access-Control-Allow-Origin 值为 * ,说明该资源可以被任意外域访问。

服务器会有的其他 CORS字 段如下:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
  1. Access-Control-Allow-Methods

这个字段表示服务器所支持的所有跨域请求的方法,这个方法指的是所有支持的方法,而不仅是浏览器请求的那个方法;这里表示服务器支持GET, POST, PUT, DELETE, OPTIONS方法。

  1. Access-Control-Max-Age

这个字段表示本次预检请求的有效期,单位为秒;在这个有效期内,浏览器不会为同一请求再次发送预检请求;

  1. Access-Control-Allow-Headers

这个字段表示服务器支持的所有头信息字段。

预检请求完成后,浏览器就会发送实际的 CORS 请求,以下是正常请求的请求报文和响应报文:

// 请求报文
PUT http://127.0.0.1:8090/test HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
Content-Length: 2
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://127.0.0.1:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7

""

// 响应报文
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Authorization, contentType, Content-Type, Accept,Origin,User-Agent
Content-Type: application/json;charset=UTF-8
Date: Mon, 25 Jun 2018 07:15:13 GMT
Content-Length: 51

{"status":true,"code":200,"message":"操作成功"}

解决跨域的几种方法

Spring 从 4.2 版本开始增加了对 CORS 的支持,以下是 Spring MVC 中的几种配置方法。

使用 @CrossOrigin 注解

我们可以在控制器请求方法(即 @RequestMapping 注解的方法)添加 @CrossOrigin 注解来启用 CORS ,默认情况下 @CrossOrigin 允许在 @RequestMapping 注解中指定的所有源和 HTTP 方法:

@RestController
public class TestController {

    @RequestMapping("/test")
    @CrossOrigin
    public JsonResult test() {
        return JsonResult.success();
    }
}

也可以为整个控制器启用CORS:

@CrossOrigin
@RestController
public class TestController {

    @RequestMapping("/test")
    public JsonResult test() {
        return JsonResult.success();
    }
}

跨域请求成功

使用 XML 配置文件

在 Spring 配置文件中添加以下配置:

<mvc:cors>
    <mvc:mapping path="/api/**"
        allowed-origins="*"
        allowed-methods="GET,POST,DELETE,PUT"
        allow-credentials="true"
        allowed-headers="Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With"
        max-age="3600"/>

    <mvc:mapping path="/resources/**"
        allowed-origins="http://www.lwhweb.com" />
</mvc:cors>

基于过滤器的 CORS 支持

新增 Filter:

public class CORSFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin","*");
        response.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age","3600");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, contentType, Content-Type, Accept,Origin,User-Agent");
        chain.doFilter(request, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

修改 web.xml 配置:

<filter>
  <filter-name>CORS</filter-name>
  <filter-class>com.isp.filter.CORSFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>CORS</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

如果是 Spring Boot 则可以直接声明过滤器使用:

@Configuration
public class MyConfiguration {

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://www.lwhweb.com");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

Spring Boot 中基于 WebMvcConfigurationSupport 配置

新增配置类 CorsConfig,继承 WebMvcConfigurationSupport

@Configuration
public class CorsConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedHeaders("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Authorization", "Cache-Control", "Content-Type")
                .maxAge(3600);
    }
}

参考文档

  1. CORS support in Spring Framework

  2. 跨源资源共享(CORS)

  3. 跨域资源共享 CORS 详解

文章目录