详解跨域问题出现及解决的原理

在本机使用浏览器访问远程服务器进行开发时,我们经常会碰到这种报错信息:

XMLHttpRequest cannot load xxxxxxxx. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.

了解过的人一眼就看出了是出现了跨域问题。

那么,为什么会出现这种情况呢?

出于对浏览器安全的考量,你在载入其他网站的资源时会触发跨域资源共享CORS这一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

跨域资源共享(CORS)机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中使用 CORS,以降低跨域 HTTP 请求所带来的风险。

同源的含义是:A网页设置的 Cookie等消息,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是:同协议、同域名、同端口

如果非同源,以下行为将会受到限制:

(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。

利用Postman进行开发或者后端获取http数据的话,是不会发送OPTIONS进行请求的,即是不会出现跨域问题。跨域问题只有在浏览器才会出现,javascript等脚本的主动http请求才会出现跨域问题。

一、跨域的两种请求

而浏览器将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

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

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

二、简单请求

对于简单请求,在头信息之中,增加一个Origin字段,直接发出CORS请求。

GET /cors HTTP/1.1
Origin: null
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

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

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应(只要有包含正确头信息即可,此时从状态码中无法看出错误,需要观察头信息)。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。

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

Access-Control-Allow-Origin: *
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字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

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

(3)Access-Control-Expose-Headers

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

三、非简单请求

3.1 预检请求

对于一些可能对服务器数据有特殊要求或有影响的请求,如 PUT,DELETE 和搭配某些 MIME 类型的 POST 方法(常见的application/json也是非简单请求),浏览器必须先发送一个“预检请求”——也就是preflight response,来确认服务器是否允许该请求,允许的话再真正发送相应的请求。

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

curl -X OPTIONS http://example.org -i

响应报文包含一个首部字段,该字段的值表明了服务器支持的所有 HTTP 方法:

为了检测后端服务器是否会出现跨域问题,我们可以使用以下命令模拟浏览器的OPTIONS预检请求:

HTTP/1.1 200 
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, x-requested-with, Token
Access-Control-Max-Age: 30
...

我们也可以使用JS脚本来判断服务器允许跨域:

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 /example.org HTTP/1.1
Origin: http://zeng.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
...

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

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

(1)Access-Control-Request-Method

必须字段,列出浏览器的CORS请求所用到的HTTP方法。

(2)Access-Control-Request-Headers

非必须字段,该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。

3.2 预检请求的回应

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

以下是服务器同意了预检请求的返回头信息:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://zeng.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
....

(1)Access-Control-Allow-Origin

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

(2)Access-Control-Allow-Methods

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

(3)Access-Control-Allow-Headers

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

(4)Access-Control-Allow-Credentials

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

(5)Access-Control-Max-Age

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

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

从上面可以看出,在服务器中可以使用过滤器进行判断来源的有效性判断。

如果服务器通过了"预检"请求,则会返回正常的状态码以及包含CORS相关的头信息字段,需要的最少信息如下:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://zeng.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
....

如果服务器不通过"预检"请求,同样是返回正常的状态码,但是不设置任何CORS相关的头信息字段,这样浏览器便不会发送相应请求,避免了用户通过其他非法来源访问服务器,降低用户出现损失的可能。

HTTP/1.1 200 OK
....

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

四、跨域问题的解决

既然了解了跨域问题出现的原因,那么解决方法的原理也都千篇一律了,都是通过定义响应中的CORS头信息进行实现的,浏览器检查到OPTIONS的响应头信息上包含:Access-Control-Allow-OriginAccess-Control-Allow-Methods,即代表服务器允许跨域。

那么有以下几种跨域实现方式:

过滤器允许跨域使用原生代码,使用Servlet便可以使用。

CorFilter、WebMvConfigurer、@CrossOrigin 需要 SpringMVC 4.2以上版本才支持,对应于springBoot 1.3版本以上

过滤器跨域、CorFilter、WebMvConfigurer属于全局 CORS 配置,@CrossOrigin属于局部 CORS配置。如果使用了局部跨域是会覆盖全局跨域的规则,所以可以通过 @CrossOrigin 注解来进行细粒度更高的跨域资源控制

注意:

4.1 使用过滤器允许跨域

通过上文我们可以知道,如果要允许跨域,只需要在浏览器发送的预检请求OPTIONS上设置必须的CORS响应头信息即可允许跨域。

@WebFilter(filterName = "cors", urlPatterns = {"/*"})
public class CorsFilter implements Filter {
    public void init(FilterConfig filterConfig) {
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        // 设置允许源,若出于安全,可以设置允许访问的用户源
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 这里通过判断请求的方法,判断此次是否是预检请求,如果是,则进行跨域的设置
        if ("OPTIONS".equals(request.getMethod())){
            // 设置状态码为204,允许跨域
            response.setStatus(HttpStatus.SC_NO_CONTENT); 
            // 必选,表明服务器支持的所有跨域请求的方法。
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS");
            // 可选, 设定允许请求的头部类型
            response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, Token"); 
            // 可选,预检有效保持时间
            response.addHeader("Access-Control-Max-Age", "1800");
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }
}

4.2 CORS全局配置

这类似于使用过滤器,但可以在Spring MVC中声明。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                // 是否发送Cookie
                .allowCredentials(true)
                // 放行哪些原始域
                .allowedOrigins("*")
                // 允许的请求类型
                .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
                // 允许的请求头
                .allowedHeaders("*")
                // 允许OPTION请求携带的字段,相当于Access-Control-Expose-Headers
                .exposedHeaders("*");
    }
}

4.3 CorsFilter(全局跨域)

在任意配置类,返回一个 新的 CorsFIlter Bean ,并添加映射路径和具体的CORS配置路径。

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        // 添加 CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        // 放行哪些原始域
        config.addAllowedOrigin("*");
        // 是否发送 Cookie
        config.setAllowCredentials(true);
        // 放行哪些请求方式
        config.addAllowedMethod("*");
        // 放行哪些原始请求头部信息
        config.addAllowedHeader("*");
        // 暴露哪些头部信息
        config.addExposedHeader("*");
        // 添加映射路径
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**",config);
        // 返回新的CorsFilter
        return new CorsFilter(corsConfigurationSource);
    }
}

4.4 使用注解 (局部跨域)

@CrossOrigin:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {

    String[] DEFAULT_ORIGINS = { "*" };

    String[] DEFAULT_ALLOWED_HEADERS = { "*" };

    boolean DEFAULT_ALLOW_CREDENTIALS = true;

    long DEFAULT_MAX_AGE = 1800;

    /**
     * 同origins属性
     */
    @AliasFor("origins")
    String[] value() default {};

    /**
     * 所有支持域的集合,例如"http://zeng.com"。
     * 这些值都显示在请求头中的Access-Control-Allow-Origin
     * "*"代表所有域的请求都支持
     * 默认支持所有请求域
     * @see #value
     */
    @AliasFor("value")
    String[] origins() default {};

    /**
     * 允许请求头重的header,默认都支持
     */
    String[] allowedHeaders() default {};

    /**
     * 响应头中允许访问的header,默认为空
     */
    String[] exposedHeaders() default {};

    /**
     * 请求支持的方法,例如"{RequestMethod.GET, RequestMethod.POST}"}。
     * 默认支持RequestMapping中设置的方法
     */
    RequestMethod[] methods() default {};

    /**
     * 是否允许cookie随请求发送,使用时必须指定具体的域
     */
    String allowCredentials() default "";

    /**
     * 预请求的结果的有效期,默认30分钟
     */
    long maxAge() default -1;

}

在控制器上使用注解 @CrossOrigin:

@RestController
@CrossOrigin
public class HelloController {
    @PostMapping("/hello")
    public String hello() {
        return "hello world";
    }
}

复制代码

在方法上使用注解 @CrossOrigin:

	@RequestMapping("/hello")
    @CrossOrigin(origins = "*")
    public String hello() {
        return "hello world";
    }

4.5 手动设置响应头(局部跨域)

使用 HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里 Origin的值也可以设置为 “*”,表示全部放行。该方法使用不方便,使用较少。

    @RequestMapping("/index")
    public String index(HttpServletResponse response) {
        response.addHeader("Access-Allow-Control-Origin","*");
        return "index";
    }

五、参考网站

浏览器同源政策及其规避方法

跨域资源共享 CORS 详解

HTTP访问控制(CORS)

Spring MVC 4.2 增加 CORS 支持

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章