Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

基于最新Spring 5.x,详细介绍了跨域的概念,以及Spring项目中如何通过CORS的方法来解决跨域问题。

本次我们来学习跨域的概念,以及Spring项目中如何通过CORS的方法来解决跨域问题,这是一种非常常见且简单的解决跨域问题的方法。

Spring MVC学习 系列文章

Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例

Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】

Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解

Spring MVC学习(9)—项目统一异常处理机制详解与使用案例

Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码

Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

1 同源和跨域

什么是同源?请求的“协议+ip(域名)+端口”被称之为“源”,如果当前页面的url和在当前页面中要访问的url具有相同的源,那么我们称这两个请求“同源”,即它们来自或者去往同一个服务器。

什么是跨域?如果当前页面的url和请求的url非同源,那么我们就可以称之为跨域,也称之为跨源!或者说,当请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域!非同源和跨域,可以说是一种事务的不同说法!

假设某个页面的URL为http://store.company.com/dir/page.html,下面是常见的跨域(非同源)请求的类型:

请求的URL是否跨域(非同源)原因
http://store.company.com/dir2/other.html只有访问的资源路径不同
http://store.company.com/dir/inner/another.html只有访问的资源路径不同
https://store.company.com/dir/page.html协议不同
http://store.company.com:81/dir/page.html端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/page.html主机(域名)不同

什么是同源策略?同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,它的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,很容易受到 XSS、 CSFR 等攻击。

同源策略对跨域(非同源)请求进行了如下限制:

  1. 无法读取非同源网页的 Cookie、LocalStorage 和IndexedDB;当然如果两个网页一级域名相同,只是次级域名不同,那么Cookie可以通过设置相同的domainName来实现共享Cookie。
  2. 无法获取非同源网页的 DOM树和Js对象。
  3. 无法获取非同源地址返回的响应,例如通过XMLHttpRequest发送的AJAX请求,以及<img>标签请求的非同源图片资源,获取想要加载的各种网络字体、样式资源。

同源策略的限制通常是浏览器自己来实现的,当同源策略触发时:

  1. 对于某些浏览器,可能已经正常发送了跨域请求,甚至服务器已经正常处理了请求并且返回了响应,只不过在处理结果时浏览器做了限制,导致无法获取服务器响应的结果!
  2. 对于另一些浏览器以及某些请求方式,它会首先发送一个OPTIONS 预检请求到服务器,以获知服务器是否允许该实际请求,当服务器响应允许此次跨域时,才会发送真正的请求并且正确的处理响应结果。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

2 模拟跨域

下面是一个没有解决跨域问题的web项目的Controller:

@RestController
public class AccessControlController {
    
    @GetMapping("/accessControl/{id}")
    public User accessControl(@PathVariable Long id, String name) {
        System.out.println(id);
        System.out.println(name);
        return new User(id, name);
    }
}

在Chrome浏览器中,当我们直接在浏览器中输入URL访问/accessControl/123?name=test时,是没问题的:

在这里插入图片描述

如果我们将此url放到另一个非同源的url页面中通过AJAX来访问时,就会出现跨域问题!

我们的js脚本如下:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8081/mvc/accessControl/123?name=test',true);
xhr.send();
xhr.onreadystatechange=function() {
    if(xhr.readyState == 4) {
        if(xhr.status == 200) {
          console.log(JSON.parse(xhr.responseText));
        }
    }
}

想要调试,非常简单,对于在Chrome浏览器中来说,我们首先随便打开一个非同源的网页,比如说百度首页https://www.baidu.com/,然后F12 检查,切换到Console,即可在这里在线执行js脚本!

输入js脚本并执行,结果如下:

在这里插入图片描述

可以看到,控制台出现了如下报错:

Access to XMLHttpRequest at 'http://localhost:8081/mvc/accessControl/123?name=test' from origin 'https://www.baidu.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

这个异常描述为响应缺少Access-Control-Allow-Origin头字段,这个字段我们下面会介绍!但是我们在后端服务器控制台能够看到正常输出:

在这里插入图片描述

这说明,实际上请求已经被发送了,并且服务器正常处理了,但是响应结果被浏览器的同源策略拦截了!通常,当前端页面的控制台输出上面的异常信息时,就是说明遇到跨域问题了!

3 解决跨域

同源策略虽然带来一定的安全性,但是这往往给一些正规的跨域需求带来不便,特别是前后端分离以及服务化的项目,通常不同的服务之间是不同源但,但是它们之间需要互相调用那个。为此,我们可以采取一些方式来绕过同源策略!

解决的方案有很多,比如JSONP、CORS、WebSocket、Nginx反向代理等。

  1. JSONP是一种只需要前端处理的比较简单的方法,但是只能支持get请求,不支持其他类型的请求,因此用的不多,这里不赘述。但是JSONP的优势在于支持某些老式浏览器,以及可以向不支持CORS的网站请求数据。
  2. WebSocket则是另一种基于HTTP升级而来的协议,它不实行同源策略,只要服务器支持,就可以通过它进行跨域通信。但是WebSocket协议的前后端代码开发和普通HTTP开发有很大区别,通常是被用于即时通信或者要求服务器实时推送数据的项目中,普通web项目为了解决跨域问题而使用WebSocket是得不偿失的!
  3. Nginx反向代理,也是一种非常有效的解决跨域问题的方法,用的也非常多,但是需要Nginx服务器的相关知识,我们后面会专门讲解!

本次我们要学习的就是Spring MVC基于CORS来解决跨域问题,这也是Spring官网推荐的方式,值得学习:https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/spring-framework-reference/web.html#mvc-cors

4 CORS概述

CORS(Cross-origin resource sharing),通俗地译为跨域资源共享,但是我认为更准确的叫法是跨源资源共享,CORS是一个W3C标准,是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(源,包括域、协议和端口),这样浏览器可以访问加载这些资源

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的OPTIONS“预检”请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。

CORS可以看作是跨源AJAX请求的一种根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

5 CORS标准

WordC为CORS标准新增了一组 HTTP 首部字段,以允许服务器在响应头中通过这些字段声明和控制哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

不同的请求场景下,CORS有不同的处理规范。CORS将请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

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

  1. 请求方法: GET、HEAD、POST三者之一;
  2. 首部字段:除了被用户代理自动设置的首部字段(例如host、connection、user-agent等)之外,其他请求首部字段只能是如下字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type。
  3. Content-Type的值只限于application/x-www-form-urlencoded、multipart/form-data、text/plain。

不同浏览器的实现可能有细微的区别!

5.1 简单请求处理

对于简单非同源请求,浏览器不会先发出预检请求,而是直接发出CORS请求

我们最初始的模拟跨域的测试案例中,发出的AJAX请求并没有定义其他头部信息,这就属于简单请求,我们切换到Chrome浏览器的Network页面,查看请求头信息如下:

GET /mvc/accessControl/123?name=test HTTP/1.1
Host: localhost:8081
Connection: keep-alive
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Mobile Safari/537.36
Accept: */*
Origin: https://www.baidu.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

简单CORS请求和普通请求的区别就是会在请求首部添加了一个origin字段,该字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。在上面头的信息中Origin为https://www.baidu.com,说明本次请求来源于百度的页面!

服务器根据请求头信息中的Origin值决定是否同意这次请求。如果Origin指定的源,不在许可范围内,服务器会直接返回一个正常的HTTP响应。浏览器接受响应之后,会判断如果响应头信息中没有包含一个名为Access-Control-Allow-Origin字段,就直接抛出一个异常,被XMLHttpRequest的onerror回调函数捕获,并且请求发起方无法获取正常的响应结果。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200,通常这种错误在浏览器控制台查看!

在这里插入图片描述

在模拟跨域的案例中,服务器没有进行CORS处理,因此返回的响应头中没有包含Access-Control-Allow-Origin字段:

在这里插入图片描述

Access-Control-Allow-Origin头部字段是服务器返回的,它的值用于指示服务器允许的域,返回表示允许所有外域访问!如果响应存在该字段,但是值不是“”也不是当前的页面源,那么请求方同样无法收到响应结果。

如果服务器仅允许来自http://localhost:8081的源访问,首部字段的内容如下:

Access-Control-Allow-Origin: http://localhost:8081

使用 Origin 和 Access-Control-Allow-Origin 字段就能完成最简单的自定义跨域访问控制,这也是为什么说CORS是基于HTTP头的机制!

如果服务器允许外部来源访问,那么响应头信息可能如下:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: https://www.baidu.com
Access-Control-Allow-Credentials: true
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 02 Feb 2021 07:57:22 GMT

另一个响应头Access-Control-Allow-Credentials是可选的,它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,那么该响应中就没有该字段。

5.1.1 附带身份凭证信息

对于跨源的XMLHttpRequest 或 Fetch 请求,浏览器默认不包含Cookie信息以及HTTP认证信息等身份凭证信息。如果需要发送凭证信息(比如Cookie),首先需要服务器统一,指定响应头包括Access-Control-Allow-Credentials字段。

另外,前端开发者还需要在AJAX中打开withCredentials属性:

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以将withCredentials设置为false,以显式关闭。

需要注意的是,如果要发送附带身份凭证的请求,响应中的Access-Control-Allow-Origin字段就不能设为“*”,必须指定明确的与请求源网页一致的域名(如上案例,Allow-Origin只能是https://www.baidu.com)。

同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

5.2 非简单请求处理

对于非简单非同源请求,比如PUT、DELETE请求,或者Content-Type字段的类型是application/json等请求,浏览器不会直接发出真实请求,而是先发出一次OPTIONS预检(preflight)请求,该请求是浏览器控制的,对于用户和前端开发人员来说是无感的!也就是说,对于非简单请求,将会分为两个请求发送,第一个是预检请求,通过之后才会发送真实请求

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和请求头字段。只有得到肯定答复,并且和当前请求匹配,浏览器才会发出正式的XMLHttpRequest请求,否则同样报错,并且不会发出真实请求。

要测试预检请求,也很简单,我们在AJAX中随便加一个请求头即可:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8081/mvc/accessControl/123?name=test',true); 
xhr.setRequestHeader('xx', 'yy');     //添加一个请求头
xhr.send();
xhr.onreadystatechange=function() {
    if(xhr.readyState == 4) {
        if(xhr.status == 200) {
          console.log(JSON.parse(xhr.responseText));
        }
    }
}

Chrome浏览器中再次发送,请求预检请求的头信息如下:

OPTIONS /mvc/accessControl/123?name=test HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: GET
Access-Control-Request-Headers: xx
Origin: https://www.baidu.com
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Mobile Safari/537.36
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Sec-Fetch-Dest: empty
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

可以看到,预检请求和正式请求时统一路径,但是请求方法是OPTIONS,OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。OPTIONS方法不会对服务器资源产生影响,不会执行控制器方法中的业务代码!

请求头中同样有一个origin字段表示请求来源!除此之外,新携带了两个首部字段:

access-control-request-method: GET
access-control-request-headers: xx
  1. Access-Control-Request-Method:必须的字段,用于告知服务器,实际CORS请求将使用的GET方法。
  2. Access-Control-Request-Headers非必须的字段,用于告知服务器,实际请求将携带一个自定义请求首部字段名:xx。如果存在多个字段,则使用“,”分隔。

服务器据此决定该实际请求是否被允许!服务器在收到预检请求之后,如果没有进行特殊CORS处理,那么便不会返回特殊的响应头,浏览器收到响应之后边抛出异常,不再发送后续真实的请求。

如果服务器进行了CORS处理并且通过了校验,那么返回的响应头信息可能如下:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: https://www.baidu.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: xx
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1800
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Content-Length: 0
Date: Tue, 02 Feb 2021 07:41:46 GMT

其中包括几个特殊字段:

Access-Control-Allow-Origin: https://www.baidu.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: xx
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1800
  1. Access-Control-Allow-Origin:必须的字段,服务器允许的域,允许所有域设置为 *。
  2. Access-Control-Allow-Methods: 服务器允许的请求方法,允许所有方法设置为*。
  3. Access-Control-Allow-Headers:如果请求包括Access-Control-Request-Headers字段,则响应的Access-Control-Allow-Headers字段是必需的。表明服务器允许添加的请求头字段,采用“,”分隔,不限于浏览器在预检请求中传递的字段。
  4. Access-Control-Allow-Credentials:处理方式与简单请求时一致。
  5. Access-Control-Max-Age: 该预检请求响应的有效时间,单位秒时。在有效时间内,浏览器无须为同一请求再次发起预检请求。浏览器自身同样维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

如果通过了预检请求,那么继续发送真实请求,并且在预检响应的有效时间内,浏览器无须为同一请求再次发起预检请求。后续的请求就是简单请求一样,只需要Origin和Access-Control-Allow-Origin字段即可!

5.3 总结首部信息

根据上面的介绍,我们总结一下CORS的解决过程中的关键首部字段。

请求首部

字段名描述
Origin表明发送预检请求或发送实际请求的源地址。所有的CORS请求中Origin 首部字段总是被发送。
Access-Control-Request-Method用于预检请求,用于将实际请求所使用的HTTP方法告诉服务器。
Access-Control-Request-Headers用于预检请求,用于将实际请求所携带的自定义首部字段告诉服务器。

响应首部

字段名描述
Access-Control-Allow-Origin必定返回的字段。指定允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符“”,表示允许来自所有域的请求。如果服务端指定了具体的域名而非“”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。
Access-Control-Max-Age指定了预检(preflight)请求的结果能够被缓存多久,单位秒。
Access-Control-Allow-Credentials是否允许请求中携带身份认证信息,比如Cookie。该功能还需要XMLHttpRequest的credentials设置为true。
Access-Control-Allow-Methods用于预检请求的响应,指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Headers用于预检请求的响应,指明了实际请求所允许携带的自定义首部字段。
Access-Control-Expose-Headers让服务器把允许浏览器访问的头放入白名单,多个头采用“,”分隔。在跨源访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。

6 CORS实现

根据上面的CORS标准,我们知道,除了携带身份认证信息的请求之外,其他简单或者复杂请求的跨源控制都是由后端来实现的。下面我们来看看基于Spring MVC的Web项目是如果通过CORS来实现跨源访问控制的!

Spring MVC的HandlerMapping的实现为CORS提供了内置支持。在成功将request映射到handler处理器后,HandlerMaping 实现将检查给定request和handler的 CORS 配置信息,随后,将会直接处理预检请求,同时拦截、验证简单和实际 CORS 请求,并设置所需的(配置的)CORS响应头信息。

为了启用跨源请求(如果存在Origin请求并且与当前请求的主机不一致,就被认为是跨源请求),我们需要具有一些显式声明的 CORS 配置。如果未找到匹配的 CORS 配置,则预检请求将直接被拒绝,并且不会向简单和实际 CORS 请求的响应中添加 CORS 头,因此在浏览器收到响应会拒绝响应结果,并在控制台抛出异常。

每个HandlerMapping都可以使用基于URL模式的 CorsConfiguration映射单独配置。在大多数情况下,web程序使用 MVC JavaConfig或 XML文件来声明这些映射规则,这会导致单个全局映射传递将给所有 HandlerMaping 实例。

我们可以将HandlerMapping级别的全局CORS配置与更细粒度的handler级别的CORS配置相结合使用。例如,基于注解的Controller控制器可以使用类或方法级的@CrossOrigin注解来为某些或者某个接口单独定义CORS配置(其他类型的handler可以实现 CorsConfigurationSource接口来配置)。

注意,如果我们具有全局和本地的CORS配置,那么对于只能接受单个值的属性(如allowCredentials和 maxAge),本地值将覆盖全局值,对于其他接受多个值的属性,比如origins,则全局和本地的CORS配置值会组合在一起(java中是list集合)!

6.1 @CrossOrigin注解配置

@CrossOrigin注解用于在基于注解的控制器方法上启用跨源请求,如下例所示:

/**
 * @author lx
 */
@RestController
public class AccessControlController {

    @CrossOrigin
    @GetMapping("/accessControl/{id}")
    public User accessControl(@PathVariable Long id, String name) {
        System.out.println(id);
        System.out.println(name);
        return new User(id, name);
    }
}

启动项目再次测试,无论是发送简单跨源请求还是复杂跨源请求,都可以成功获得响应:

在这里插入图片描述

默认情况下,@CrossOrigin注解表示允许:所有的origins(源)、所有的headers(请求头字段)、控制器方法映射到的所有 HTTP 方法。

默认情况下,allowedCredentials属性为false,也就是不允许附带身份凭证信息,比如Cookie和CSRF token,通常手动设置为true。

默认情况下,maxAge被设置为30分钟。

@CrossOrigin还支持标注在类上,并且由所有控制器方法将继承该配置,并且同样遵循优先级以及复合规则:对于单个字段的配置,方法上的@CrossOrigin注解优先级更高,方法和类上的@CrossOrigin注解的值将组合(java中是list集合),如下所示:

/**
 * @author lx
 */
@RestController
public class AccessControlController {

    @CrossOrigin
    @GetMapping("/accessControl/{id}")
    public User accessControl(@PathVariable Long id, String name, HttpEntity httpEntity) {
        System.out.println(httpEntity.getHeaders());
        System.out.println(id);
        System.out.println(name);
        return new User(id, name);
    }
}

6.1.1 简单原理

类以及方法上的@CrossOrigin注解,在RequestMappingHandlerMapping被实例化之后的afterPropertiesSet回调方法中(RequestMappingHandlerMapping实现了InitializingBean接口),随着处理器方法HandlerMethod的解析被一并解析。

每解析到一个处理器方法,会尝试为当前HandlerMethod初始化一个CorsConfiguration,其内部保存了当前方法的CORS配置:

public class CorsConfiguration {
    //……

    /**
     * Access-Control-Allow-Origin
     */
    private List<String> allowedOrigins;
    /**
     * Access-Control-Allow-Methods
     */
    private List<String> allowedMethods;
    /**
     * Access-Control-Allow-Headers
     */
    private List<String> allowedHeaders;
    /**
     * Access-Control-Expose-Headers
     */
    private List<String> exposedHeaders;
    /**
     * Access-Control-Allow-Credentials
     */
    private Boolean allowCredentials;
    /**
     * Access-Control-Max-Age
     */
    private Long maxAge;

    //……
}

配置CorsConfiguration时(RequestMappingHandlerMapping#initCorsConfiguration方法),会尝试获取当前方法以及方法所属的类上的@CrossOrigin注解,随后会对他们进行合并:

在这里插入图片描述

随后会将控制器方法的HandlerMethod与对应的CorsConfiguration缓存起来(AbstractHandlerMethodMapping#register方法):

在这里插入图片描述

这样,在后续请求到来的时候,就可以根据CorsConfiguration的配置进行CORS控制了!

当请求到来时,Spring 中对 CORS 规则的校验委托给DefaultCorsProcessor的processRequest方法,而DefaultCorsProcessor在RequestMappingHandlerMapping被实例化时一起实例化的(位于AbstractHandlerMapping中)。

在这里插入图片描述

那么DefaultCorsProcessor具体是怎么调用的呢,对于预检请求实际上使用PreFlightHandler作为handler来调用的,对于其他请求则是通过HandlerInterceptor 拦截器CorsInterceptor来进行调用的,并且它位于拦截器链的首位:

在这里插入图片描述

对于预检请求,PreFlightHandler的handler仅仅调用DefaultCorsProcessor的processRequest方法而没有处理器方法的调用,这样就避免了业务代码的执行,而对于其他请求,在handler方法调用之前则会执行拦截器首位的CorsInterceptor,拦截器内部的preHandle方法中同样会调用DefaultCorsProcessor的processRequest方法来判断CORS,如果不通过,那么preHandle返回false,后续handler方法也就不再执行,同样避免了由于CORS不通过但是业务代码执行的问题!

注意,如果没有配置CorsConfiguration(无论是全局的还是局部的),或者请求不是预检请求,那么即使是CORS请求到来时,业务代码会按照正常请求流程来执行,也就不会有CORS校验,虽然浏览器不会呈现最终结果!

DefaultCorsProcessor的processRequest方法逻辑为:

  1. 判断是否是有效的CORS请求,如果request已经存在Origin请求头并且和当前服务器非同源,那么是有效的CORS请求,继续该方法的后续CORS判断,否则算作CORS校验通过,继续后续的方法处理!
  2. 如果response存在Access-Control-Allow-Origin响应头,那么表示该请求已被处理过了,算作CORS校验通过,继续后续的方法处理!否则,继续后续CORS判断。
  3. 如果CorsConfiguration为null
  4. 如果是预检请求,那么设置403响应码并返回,表示预检失败,服务器拒绝该请求;如果不是预检请求,算作CORS校验通过,继续后续的方法处理!
  5. 如果CorsConfiguration不为null,那么通过配置的CORS校验该请求头中的CORS信息,主要是校验origin 、method header是否合法(受支持)。如果全部合法,则在 response中添加响应头字段,并继续后面的处理(比如handler方法调用),如果不合法,则设置403响应码并返回,表示预检失败,服务器拒绝该请求。

6.2 全局配置

除了细粒度、控制器方法级别配置外,我们更多的可能还希望定义一些全局CORS配置。我们可以在任何处HandlerMapping上单独设置基于URL的CorsConfiguration映射。但是,大多数应用程序使用 MVC JavaConfig或 MVC XML配置来做到这一点。

默认情况下,全局配置表示允许:所有的origins(源),所有的headers(请求头字段),GET、HEAD和POST方法。

默认情况下,allowedCredentials属性为false,也就是不允许附带身份凭证信息,比如Cookie和CSRF token,通常手动设置为true。

默认情况下,maxAge被设置为30分钟。

6.2.1 Java配置

采用JavaConfig的配置方式非常简单,我们只需要实现WebMvcConfigurer接口并且,实现addCorsMappings回调方法即可,在方法中即可通过参数配置CORS信息!

/**
 * @author lx
 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    /**
     * 配置跨源请求处理。
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //为指定的路径(模式)启用跨源请求处理。
        //默认情况下,此映射的CORS配置采用CorsConfiguration.applyPermitDefaultValue()的配置
        //即允许所有的origins(源),所有的headers(请求头字段),所有的GET、HEAD和POST方法。
        registry.addMapping("/api/**")
                //设置允许访问该资源的外域源,这里是所有
                .allowedOrigins("*")
                //设置实际请求所允许使用的 HTTP 方法。这里是所有
                .allowedMethods("*")
                //设置实际请求所允许携带的自定义首部字段。这里是所有
                .allowedHeaders("*")
                //设置是否允许请求中携带身份认证信息,比如Cookie。这里是允许
                .allowCredentials(true);
    }

}

6.2.2 XML配置

若要在Spring的XML配置文件中启用CORS,可以使用<mvc:cors>标签:

<mvc:cors>
    <!--配置第一个CORS映射处理规则-->
    <mvc:mapping path="/api/**"
                 allow-credentials="true"
                 allowed-headers="*"
                 allowed-methods="*"
                 allowed-origins="*"
                 max-age="1800"/>
    <!--配置第二个CORS映射处理规则-->
    <mvc:mapping path="/service/**"/>
    <!--  ……………… -->
</mvc:cors>

6.3 CORS Filter

我们也可以通过Servlet中的Filter来实现CORS的支持,此前的老项目都是自己实现CorsFilter。Spring MVC 4.2之后,已经内置了CorsFilter的实现,我们无需自己定义,只需要配置即可。

首先,我们需要配置CorsFilter的bean:

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    // 使用默认CORS参数
    // config.applyPermitDefaultValues()
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    config.setMaxAge(1800L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

随后我们需要在web.xml中配置DelegatingFilterProxy来代理配置的CorsFilter的调用,不能直接在web.xml中配置Spring MVC的CorsFilter!

<!-- 通过DelegatingFilterProxy来代理Spring管理的CorsFilter bean-->
<filter>
    <filter-name>corsFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <!--设置是否调用代理目标bean上的 Filter.init 和 Filter.destroy的生命周期方法。-->
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
    <!--DelegatingFilterProxy的filter-name应该和Spring管理的Filter的beanName一致-->
    <!--如果不一致,需要配置targetBeanName参数指向Spring管理的Filter的beanName-->
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>corsFilter</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>corsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

CorsFilter只是 Spring MVC Java 配置CORS和 XML配置CORS的替代方案,仅适用于仅依赖于spring-web(不依赖spring-webmvc上)的应用程序,或者对于要求在Filter级别执行 CORS 检查的安全约束。实际用的并不多!

另外,如果需要使用Spring Security,那么Spring Security内置了CorsFilter的支持!

相关文章:

  1. https://spring.io/
  2. Spring Framework 5.x 学习
  3. Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值