天天看点

跨域、同源策略、CORS以及如何跨域请求资源

一、什么是跨域?

要想知道什么是跨域,那就得知道域的结构:

域 = 协议 + 主机地址 + 端口号

看几个对比网站:

http://baidu.com

https://baidu.com

是不同的域,因为请求协议不同;

http://www.baidu.com

http://zzz.baidu.com

是不同的域,因为主机地址不同;

http://www.baidu.com:8080

http://www.baidu.com:8081

是不同的域,因为端口号不同。

不写端口号通常为80端口。

跨域就是在一个域里请求另一个域,在A网站的页面,有个按钮请求了B网站的某个资源,那就是跨域。

二、什么是同源策略?

默认浏览器对跨域的JavaScript请求响应结果做出限制,如果你用JS进行了跨域请求,服务器会正常接收请求并进行处理以及响应,但是浏览器会报错,响应回来的数据不会让你使用。

三、什么是CORS?

cors就是一个w3c的标准,是一个跨域请求资源的解决方案,各家浏览器都实现了这个规范。

同源策略太暴力了吧?我发了个JS请求,你直接就把响应给我的数据给处理掉了,太霸道了,导致出现了很多稀奇古怪的解决方案,比如说麻烦并且只能处理get请求的JSONP,什么在《Script》标签里边请求,那都不正宗。

所以说w3c制订了一套规范,每个浏览器,你别直接就把人家js的跨域请求回来的资源给干掉,你每次你这么办,你让服务器做决定,到底让不让它跨域拿到资源,这多好啊。

四、浏览器对CORS的实现原理

javascript发送跨域请求的时候,浏览器会在请求头上加一个字段,叫Origin,意思就是来源,是当前域的值,也就是从哪个域向服务器发送的请求,然后浏览器不管你服务器进行了什马样的操作,反正在你给我的响应,浏览器会去响应头找它想看到的字段,并看看这个字段服务器给设置了个什么值,然后浏览器就决定这个响应回来的资源怎么处置。

第一种情况:服务器响应头没有那个字段或者值不符合预期,那浏览器就直接报错,请求失败

第二种情况:服务器响应头有那个字段,值也符合了规定,那这次请求就成功了。

进入正题

我们知道了大概的原理,那就看看请求头里的字段具体是啥?响应头里的字段又具体是个啥?

CORS规范中,Javascript的跨域请求还被分成了简单请求和不简单请求:

简单请求:常见的get/post/head请求,前提没有添加自定义的请求头字段,Content-Type为text/plain、multipart/form-data、application/x-www-form-urlencoded这几种。

非简单请求:get/post/head以外的,如put、delete方法等,或者你虽然用的是get/post/head,但是有自定义的头,或者你虽然用的是get/post/head,但是内容类型定义成了规定以外的,比如json

①当是简单请求的时候,请求头会被加上一个Origin字段,标明了来源。响应头中需要设置以下几个字段:

1)Access-Control-Allow-Origin : 这个属性字段最重要,没有浏览器就报错,说你响应中没有这东西。但是就算是有,但是服务器给这个字段设置的值,跟你当前的域不一样,也就是跟你请求头中的Origin不一样,那浏览器也一样报错,说你域跟人家服务器要求的域不一样。要是想让所有来源都能访问,那服务器就把这个属性值设成 * 。

2)Access-Control-Allow-Credentials:这是个起辅助作用的字段了,是布尔型,服务器可以不设置,不设置就是false。这个东西意思就是请求中可不可以带cookie。

3)Access-Control-Expose-Headers:也是个辅助作用的字段,意思就是请求方可以用JS获取响应头中的哪些字段,默认不设置就是6个,分别是:Cache-Control、Content-language、Content-Type、Expires、Last-Modified、Pragma。

②当是非简单请求的时候,请求头被加上Origin和Access-Control-Request-Method字段,有时也会被加上Access-Control-Request-Headers

有些请求是因为添加了自定义的请求头才成为的非简单请求,这时候请求头中就会带上Access-Control-Request-Headers参数,表明了你自定义了哪些请求头字段。

非简单请求,浏览器会在真正的请求之前先发送一次请求,因为这次请求不简单,所以可能不被服务器允许,为了节省资源,就先发送一个轻量的小请求探探路,这个请求叫预检请求。预检请求使用的请求方法是options,没有请求体,就带一些元信息在头里边,具体就是Origin和Access-Control-Request-Method。Access-Control-Request-Method的意思是后边的真正的请求用的是什么请求方法,比如说你要发送一个put请求修改某条数据,那打头的预检请求就记录真正的要改数据的这个请求用的是put方法,然后浏览器从预检请求头中拿到Origin和Access-Control-Request-Method判断让不让你这个Origin域的请求以Access-Control-Request-Method请求方式请求资源。

服务器同样要在响应头中设置一些字段,在简单请求的那种基础之上,又多了一个字段,是必须的一个字段,跟Origin一样没设置或者设置的不对浏览器都会报错。

1)Access-Control-Allow-Methods:表示允许哪些请求方法被允许跨域。要是不设置这个响应字段,那浏览器直接报错,后续的真正请求不会被执行;但是就算设置了值,但是跟后边真正请求的请求方法不一样,还是会报错,且真正的请求不会被执行。

2)Access-Control-Max-Age:代表本次预检的有效时间。以后再有符合响应头中规定的域以规定的方法发送相同接口的请求,浏览器在有效时间内就不用再发预检了,直接发真正的请求。

SpringBoot(SpringMVC)处理跨域

一、自定义过滤器

①过滤器

public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) servletResponse;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        res.setHeader("Access-Control-Allow-Methods", "*");
        res.setHeader("Access-Control-Allow-Headers", "*");

        filterChain.doFilter(servletRequest,res);
    }
}
           

②配置过滤器

此处注意过滤器的路径配置规则和springmvc的拦截器路径规则是有区别的。

过滤器/hello/,拦截带有/hello开始的所有层级请求,比如/hello/a/b会被拦截。

拦截器/hello/,只是拦截下边的一层,比如说/hello/a或者/hello/b都会被拦截,但是/hello/a/b就不会,

要想拦截多层请求,可以/hello/**。

拦截器路径比过滤器灵活了点。

@Configuration
public class CorsFilterConfiguration {

    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new CorsFilter());
        registrationBean.addUrlPatterns("/*");

        return registrationBean;
    }
}
           

二、自定义拦截器

①拦截器

public class Corsinterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "*");
        return true;
    }
}
           

②配置拦截器

@Configuration
public class CorsWebConfiguration implements WebMvcConfigurer{
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new Corsinterceptor())
                .addPathPatterns("/**");
    }
}
           

三、使用SpringMVC的注解@CrossOrigin

此注解可以加在方法上,也可以加在类上。

加在方法上,只对当前方法进行处理;

加在类上,对这个类的所有方法都进行相同的处理。

示例代码:

@RestController
public class HelloController {

    /**
     * DefaultCorsProcessor 用来处理@CrossOrigin配置的跨域逻辑
     */
    @RequestMapping("/hello")
    @CrossOrigin(origins = "*",allowCredentials = "true",allowedHeaders = "*",methods = RequestMethod.POST)
    public String hello(){
        return "hello";
    }
}
           

这是一个在普通不过的controller了,我把@CrossOrigin加在了方法上,表示只对当前这个方法有效。

使用这个注解往往会遇到很多的坑,很多人习惯直接用默认的,就直接@CrossOrigin放在方法上或者类上,用默认值,这样的确可以,但是碰到了某些情况,你会发现就是tm的跨域失败,浏览器就是报错。

首先看下这个注解的源码:

public @interface CrossOrigin {

    @AliasFor("origins")
    String[] value() default {};

    @AliasFor("value")
    String[] origins() default {};

    String[] allowedHeaders() default {};

    String[] exposedHeaders() default {};

    RequestMethod[] methods() default {};

    String allowCredentials() default "";

}
           

我对源码做了精简,这就是我们前面提到的,要在响应头中添加的一些东西,但是,我们要注意什么呢?这是springmvc的注解,既然它做出了这个注解,那就是有处理这个注解的类,经我查找,是DefaultCorsProcessor类,这个类的处理逻辑不同于我前边写的简单的拦截器和过滤器的处理逻辑,咱们写的那些是会在响应头中设置好cors相关的字段,让浏览器根据响应头中的字段进行判断跨域是否成功,但是DefaultCorsProcessor是直接在后台做了判断,如果跨域请求的条件不符合配置的条件,响应头中不会给你添加任何cors相关的字段,直接给你个状态403,并附上信息:Invalid CORS request XXX

上源码:

public class DefaultCorsProcessor implements CorsProcessor {

    @Override
    @SuppressWarnings("resource")
    public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
                                  HttpServletResponse response) throws IOException {

        //响应头中添加校验信息,证明服务器已经校验过了
        response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
        response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
        response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);

        //判断是否是跨域请求
        if (!CorsUtils.isCorsRequest(request)) {
            return true;
        }

        //判断是否已经被别的拦截器或过滤器处理过了
        if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
            logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
            return true;
        }

        //判断是否是一个预检请求
        boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);

        //没配置cors的相关信息,执行相关代码
        //但是就算咱们在@CrossOrigin没进行任何配置,也会有默认值的,默认值见下方debug贴图
        if (config == null) {
            //如果是预检请求,但是没配置cors的相关信息,直接响应一个403
            //但是就算咱们不配置,也会有默认值,所以不会执行下面代码
            if (preFlightRequest) {
                rejectRequest(new ServletServerHttpResponse(response));
                return false;
            }
            else {
                return true;
            }
        }
        //执行进一步判断,判断请求方法符不符合、请求来源是否符合配置好的域集合
        return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
    }

    /**
     * 这方法就是那个在响应头设置403,插上报错信息的公共方法
     */
    protected void rejectRequest(ServerHttpResponse response) throws IOException {
        response.setStatusCode(HttpStatus.FORBIDDEN);
        response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
        response.flush();
    }

    /**
     * 执行进一步判断,判断请求方法符不符合、请求来源是否符合配置好的域集合
     */
    protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
                                     CorsConfiguration config, boolean preFlightRequest) throws IOException {

        //这块是关键点一,如果请求来源与配置中的允许域集合没有匹配上的,就直接给你个无情403
        String requestOrigin = request.getHeaders().getOrigin();
        String allowOrigin = checkOrigin(config, requestOrigin);
        HttpHeaders responseHeaders = response.getHeaders();
        if (allowOrigin == null) {
            logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
            rejectRequest(response);
            return false;
        }

        //这是关键点二,如果请求的方法和配置中的允许方法集合没匹配上,直接无情403
        //其中getMethodToUse有点儿意思,获取请求的方法是分情况的,要是个简单请求,那过来的请求就是真正的请求,直接获取
        //这个请求是哪种请求方法就行了;
        //但是这要是一个预检请求,那预检请求的方法是不能代表真正的请求的,所以需要从请求头中拿到Access-Control-RequestMethod的值
        HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
        List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
        if (allowMethods == null) {
            logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
            rejectRequest(response);
            return false;
        }

        //这是关键点三,验证请求头中除了简单请求头以外的额外头信息列表
        //非简单请求不是有个规定么,比如说post是简单请求,但是多了自定义的头信息就变成了非简单请求
        //多的这些头信息在这里取出来进行判断
        //服务端默认配置是*,啥头都行
        List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
        List<String> allowHeaders = checkHeaders(config, requestHeaders);
        if (preFlightRequest && allowHeaders == null) {
            logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
            rejectRequest(response);
            return false;
        }

        //下边就是顺利通过了校验,进行响应头信息的设置
        responseHeaders.setAccessControlAllowOrigin(allowOrigin);

        if (preFlightRequest) {
            responseHeaders.setAccessControlAllowMethods(allowMethods);
        }

        if (preFlightRequest && !allowHeaders.isEmpty()) {
            responseHeaders.setAccessControlAllowHeaders(allowHeaders);
        }

        if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
            responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
        }

        if (Boolean.TRUE.equals(config.getAllowCredentials())) {
            responseHeaders.setAccessControlAllowCredentials(true);
        }

        if (preFlightRequest && config.getMaxAge() != null) {
            responseHeaders.setAccessControlMaxAge(config.getMaxAge());
        }

        response.flush();
        return true;
    }

}
           

@CrossOrigin默认值

跨域、同源策略、CORS以及如何跨域请求资源

可以看出,默认匹配的请求方式是get/post/head请求,默认所有域都行,默认所有多出的请求头都行,默认预检缓存在浏览器的时间是30分钟。

什么时候会走DefaultCorsProcessor中的逻辑代码?

并不是任何的请求都会被拦截到这段逻辑中,有以下几个条件:

①请求是简单请求,但是未配置@CrossOrigin注解,那么不会添加cors拦截器到执行链。

②请求是简单请求,但是配置了@CrossOrigin注解,那么会添加一个cors的拦截器到执行链中,进行执行。

③请求只要是非简单请求,就会将此拦截器加到执行链中并替换掉真正的Controller。

简单看下源码:

进入DispatcherServlet的doDispatch方法,观察getHandler(processedRequest)方法如下:

获取请求的对应处理器执行链

mappedHandler = getHandler(processedRequest);
           

进入getHandler方法,它会根据5种请求风格来匹配获取处理链,我们主要看RequestMappingHandlerMapping这种

跨域、同源策略、CORS以及如何跨域请求资源

进入mapping.getHandler(request)方法:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		.......根据请求地址获取处理链,代码省略

		//主要观察的方法
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}
           

这个if判断是我们要观察的,它的逻辑就是:

如果你配置了跨域的配置信息,比如说@CrossOrigin注解。或者请求是一个预检请求,那么就会额外获取一个处理跨域的拦截器到处理链中,我们跟进getCorsHandlerExecutionChain(request, executionChain, config)方法。

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
			HandlerExecutionChain chain, @Nullable CorsConfiguration config) {

		if (CorsUtils.isPreFlightRequest(request)) {
			HandlerInterceptor[] interceptors = chain.getInterceptors();
			chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
		}
		else {
			chain.addInterceptor(0, new CorsInterceptor(config));
		}
		return chain;
	}
           

此方法进行了一次判定,如果请求是预检请求,那么处理链中就不会包含真正处理请求的controller,处理链的主体变成了处理跨域的拦截器。

如果不是预检请求,也就是说来的是一个跨域请求,但不是预检请求,那么就将处理跨域的拦截器添加到处理链中,Controller依然是处理链的主体。

我们回到DispatcherServlet的doDispatch方法:

跨域、同源策略、CORS以及如何跨域请求资源

在获取到处理链之后,会获取一个适配器,也就是适配器模式,用适配器执行Handler,也就是处理链中的Controller和拦截器。

我截取部分代码:

//获取适配器对象,之后用适配器模式调用处理器中的相应处理方法
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				
				//调用处理链中,拦截器的前置方法,也就是拦截器的preHandle方法
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}
				
				// 如果拦截器放行了,则会执行处理器,可能是Controller,也可能是跨域处理拦截器
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				//调用处理链中,拦截器的前置方法,也就是拦截器的postHandle方法
				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
           

如果在获取处理链时,由于请求是预检请求,那么返回的处理链的主体对象是一个跨域处理对象,里边会持有其他拦截器,那么在适配器调用主体处理请求的时候,实际并不会执行到Controller的代码。

例如我这个测试所返回的处理链主体:

跨域、同源策略、CORS以及如何跨域请求资源

可以看出是一个AbstractHandlerMapping中的一个内部类对象PreFlightHandler。

适配器会调用handle方法:

跨域、同源策略、CORS以及如何跨域请求资源

handle方法里又会调用PreFlightHandler的handleRequest方法:

跨域、同源策略、CORS以及如何跨域请求资源

可以看到,此时调用了DefaultCorsProcessor中的处理方法。

跨域、同源策略、CORS以及如何跨域请求资源

DefaultCorsProcessor被包装在了跨域处理拦截对象中。

总结:

使用@CrossOrigin解决跨域,我们就要规范的使用@RequestMapping注解,get请求就是@GetMapping,post请求就是@PostMapping,put请求就是@PutMapping,delete请求就是@DeleteMapping