CORS

CORS

什么是 CORS

  • 常见前端错误信息如下
Failed to load https://example.com/: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://XXX' is therefore not allowed access. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

上面的错误是因为浏览器的CORS机制导致的

Cross-Origin Resource Sharing (CORS)

COSR(跨站点资源分享)通俗的讲是跨域问题,严格来说它是跨域问题的解决方案之一,而且是官方解决方案

在CORS成为标准之前,是没有办法请求不同域名的后端API的,因为安全原因。请求会被同源策略阻止,现在也是。

Access-Control-Allow-Origin

  • 允许任意域名

*

  • 完整的域名
https://example.com

如果你需要客户端传递验证信息到头部(比如:cookies)。这个值不能为 *

Access-Control-Allow-Credentials

这个头部信息只会在服务器支持通过cookies传递验证信息的返回数据里。

它的值只有一个就是 true

跨站点带验证信息时,服务器必须要争取设置这个值,服务器才能获取到用户的cookie。

Access-Control-Allow-Headers

提供一个逗号分隔的列表表示服务器支持的请求数据类型。

假如你使用自定义头部(比如:x-authentication-token 服务器需要在返回OPTIONS请求时,要把这个值放到这个头部里,否则请求会被阻止)。

Access-Control-Expose-Headers

相似的,这个返回信息里包含了一组头部信息,这些信息表示那些客户端可以使用。

其他没有在里面的头部信息将会被限制(译者注:这个头信息实战中使用较少)。

Access-Control-Allow-Methods

一个逗号分隔的列表,表明服务器支持的请求类型(比如:GET, POST)

Origin

这个头部信息,属于请求数据的一部分。这个值表明这个请求是从浏览器打开的哪个域名下发出的。出于安全原因,浏览器不允许你修改这个值

CORS 的一点说明

但我们首先要明确以下几点

(1)跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/java等其它环境

(2)跨域请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。

换句话说,浏览器安全的基石是同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。

这是一个用于隔离潜在恶意文件的重要安全机制。

是什么

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

它通过服务器增加一个特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。

CORS Header
Access-Control-Allow-Origin: http://www.xxx.com
Access-Control-Max-Age:86400
Access-Control-Allow-Methods:GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Credentials: true

含义解释:

CORS Header属性解释
Access-Control-Allow-Origin 允许http://www.xxx.com域(自行设置,这里只做示例)发起跨域请求
Access-Control-Max-Age 设置在86400秒不需要再发送预校验请求
Access-Control-Allow-Methods 设置允许跨域请求的方法
Access-Control-Allow-Headers 允许跨域请求包含content-type
Access-Control-Allow-Credentials 设置允许Cookie

后端解决方式

@CrossOrigin 注解

简单粗暴的方式,Controller 层在需要跨域的类或者方法上加上该注解即可

@RestController
@CrossOrigin
@RequestMapping("/situation")
public class SituationController extends PublicUtilController {
    @Autowired
    private SituationService situationService;
    
    // log日志信息
    private static Logger LOGGER = Logger.getLogger(SituationController.class);
   
    // ...
}

MVC 配置

增加一个配置类,CrossOriginConfig.java。继承WebMvcConfigurerAdapter或者实现WebMvcConfigurer接口,其他都不用管,项目启动时,会自动读取配置。

@Configuration
@EnableAsync
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("*")
                // 允许凭证,解决session跨域丢失问题
                .allowCredentials(true)
                .maxAge(3600)
                .exposedHeaders(
                        "Access-Control-Allow-Origin",
                        "Access-Control-Allow-Methods",
                        "Access-Control-Allow-Headers",
                        "Access-Control-Max-Age",
                        "Access-Control-Request-Headers",
                        "Access-Control-Request-Method"
                );
    }
}

采用过滤器(filter)的方式

同方法二加配置类,增加一个CORSFilter 类,并实现Filter接口即可,其他都不用管,接口调用时,会过滤跨域的拦截。

@Component
public class CORSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        res.addHeader("Access-Control-Allow-Credentials", "true");
        res.addHeader("Access-Control-Allow-Origin", "*");
        res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
        res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
        res.setHeader("Access-Control-Max-Age", "3600");

        if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
            response.getWriter().println("ok");
            return;
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }

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

CORS 的两次请求

跨域请求的时候,会出现两次请求。

一次 OPTION, 另一个才是真正的 GET/POST。

原因

第一个OPTIONS的请求是由Web服务器处理跨域访问引发的。

网上资料显示,OPTIONS是一种“预检请求”,浏览器在处理跨域访问的请求时如果判断请求为复杂请求,则会先向服务器发送一条预检请求, 根据服务器返回的内容浏览器判断服务器是否允许该请求访问。

如果 web 服务器采用 cors 的方式支持跨域访问,在处理复杂请求时这个预检请求是不可避免的。

查询代码发现,我们的web服务器确实采用的是cors来解决跨域访问的问题,并且我们在header中添加了自定义参数,导致我们的每次请求都为复杂请求,从而产生了每次请求都会发送两条请求的现象。

简而言之

OPTIONS请求方法的主要用途有两个:

1、获取服务器支持的HTTP请求方法;也是黑客经常使用的方法。

2、用来检查服务器的性能。

例如:AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。

优化

问题的原因找到了,就要想办法解决这个问题。既然浏览器在处理复杂请求时,不可避免的要发送预检请求,那么能否减少预检请求的次数呢?

比如,预检一次设置一个有效期,在有效期内不再重复预检。

顺着这个思路,继续搜索相关资料,最终发现设置 Access-Control-Max-Age 这个参数即可达到预期目标。

该参数用来指定本次预检请求的有效期,单位为秒。在服务器上设置该参数之后,问题解决了。

CORS 配置无效

现象

按照网上说的 2 种配置方法,发现都没有用。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;

/**
 * @since 1.0.0
 * @author binbin.hou
 */
@Configuration
public class MvcConfigurer extends WebMvcConfigurerAdapter {

    @Autowired
    private SessionRequestInterceptor sessionRequestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(sessionRequestInterceptor)
                .addPathPatterns("/**");
        super.addInterceptors(registry);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600);
    }

    /**
     * cors
     *
     * https://blog.csdn.net/saytime/article/details/74937204
     * https://blog.csdn.net/xy631739211/article/details/111812803
     * @return 过滤器
     */
    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }

}

原因

拦截器的优先级问题,调整如下:

/**
 * cors
 *
 * https://blog.csdn.net/saytime/article/details/74937204
 * https://blog.csdn.net/xy631739211/article/details/111812803
 *
 * https://blog.csdn.net/z69183787/article/details/109488322
 * @return 过滤器
 */
private CorsConfiguration buildConfig() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedOrigin("*");
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.addAllowedMethod("*");
    corsConfiguration.setAllowCredentials(true);
    return corsConfiguration;
}

@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", buildConfig());
    return new CorsFilter(source);
}

参考资料

跨域资源共享 CORS 详解

https://segmentfault.com/q/1010000008992437

https://blog.csdn.net/cnhnnyzhy/article/details/53128179

https://zhuanlan.zhihu.com/p/29980092

https://zhuanlan.zhihu.com/p/31935253

Springboot处理CORS跨域请求的三种方法