CORS 跨域介绍
已知
不得不说,我(们)对跨域可能有些误解。确切地讲,是对跨域的使用有些误解。
我的跨域知识从哪里来?从这里——跨源资源共享(CORS) - HTTP | MDN
能总结出几点
- 跨域是一种允许服务端对来自浏览器的访问进行控制的机制
- 它涉及到一系列专用的请求头
- 浏览器端
- Origin:声明源站
- Access-Control-Request-Method:声明自己将要使用什么方法
- Access-Control-Request-Headers:声明自己将要携带哪些自定义头部
- 服务端
- Access-Control-Allow-Origin:声明本站允许的源站
- Access-Control-Allow-Methods:声明本站允许的方法
- Access-Control-Allow-Headers:声明本真允许的自定义头部
- Access-Control-Max-Age:声明一个预检请求的有效期,有效期内同一个请求无须再次预检
- 浏览器端
- 请求被划分为简单请求和复杂请求
- 复杂请求在请求实际发出之前,会发出OPTIONS预检请求进行跨域询问
- 简单请求不需要预检
在实际使用中,我们为了方便,尝尝会将服务端的所有响应配置为“*”,即允许所有源站、方法、自定义首部的请求进行访问。
然而,这招在需要传输Cookie的请求中,行不通,浏览器会报如下错误。
1 | xxx from origin 'xxxx' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute. |
盲区
出现这个问题,是因为漏掉了关键的一点:附带身份凭证的请求。其明确规定一点
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”
相应的,它必须是确切地和请求中的Origin进行匹配,即,如果请求的Origin是http://xyc.com,则响应的Access-Control-Allow-Origin也必须为http://xyc.com
这一点可以验证,并且牵出一个新的响应请求头
- Access-Control-Allow-Credentials: 是否允许跨域携带凭证。为true时,代表允许,相应地Access-Control-Allow-Origin 就一定不会为“*”
出现上面的错误只有一种情况,即响应只有Access-Control-Allow-Origin: *,而没有Access-Control-Allow-Credentials,或者其值为false。
解决办法,加上Access-Control-Allow-Credentials: true,并修改Access-Control-Allow-Origin。
Spring对跨域的支持
所有Spring Web模块,都支持直接配置跨域。下面是正确且唯一正确的配置
web服务的代码中配置
1 |
|
Spring Gateway中配置文件的配置
1 | # /**表示配置针对所有路径 |
这里尤其注意区分
addAllowedOriginPattern("*")
和addOrigins("*")
的区别
前者的*表示通配符,用来匹配请求的Origin,如果匹配成功,响应的Access-Control-Allow-Origin就直接被设置为请求的Origin
后者表示直接将所有的Access-Control-Allow-Origin值都设置为”*“。这个在和allowCredentials=true一起使用的情况下要注意
如果两个都设置,将会是
addOrigins("*")
生效。这一点可以去翻源码位置:org.springframework.web.cors.CorsConfiguration#checkOrigin
请求头冲突的问题
如果使用Spring Gateway作为网关,后面再挂着Spring Web作为服务,并且它们都正确配置了跨域。那么正常情况下,响应的请求头就会出现两份完全一样的跨域头部,像下面这样
1 | vary: Origin |
这似乎不违反HTTP关于请求头重复的规定,但在跨域的机制中,却是不可接收的,可能出现这种错误
1 | Access to XMLHttpRequest at 'xxxxx' from origin 'http://localhost:3200' has been blocked by CORS policy: |
解决办法就是在网关对重复的头部进行处理,我们可以用过滤器:DedupeResponseHeader
1 | spring.cloud.gateway.default-filters[0]=DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials |
它默认保留重复头中的第一个,具体参见手册
Vary怎么说
我们总是能看到响应中有Vary头部,它有什么用?服务端是如何生成的?
根据标准,它是被服务器用来声明一个请求的响应的变化依据,浏览器可用它来控制缓存。比如对Vary: Origin
,它表示,对同一个请求,服务器针对不同的源站有不同的响应。
服务端是如何生成的呢?确切地讲,不同场景下有不同的生成方式,比如Spring Web中如果开启了跨域,默认会在响应上添加Vary请求头,写死的,其值包含
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
源码参见:org.springframework.web.cors.reactive.DefaultCorsProcessor#VARY_HEADERS
1 | private static final List<String> VARY_HEADERS = Arrays.asList( |
Spring Cloud同时使用DedupeResponseHeader和WebSocket
Spring Cloud中同时使用DedupeResponseHeader和WebSocket会报错。如下,这是握手成功后抛出的异常。
1 | java.lang.UnsupportedOperationException |
原因
Spring Cloud对WebSocket有特殊处理,位于过滤器WebsocketRoutingFilter中,与这部分相关的逻辑位于:org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy#upgrade
1 |
|
其中的关键点是,response.setComplete(),这里已经将response设置为完成状态,后续过滤器就无法再操作其内容,而我们设置的DedupeResponseHeader过滤器就在它的后面,因此报错。
解决
这里,要明白两个点
- WebSocket握手成功后的响应头,不会存在重复的情况,这一点由WebSocket相关的Filter自己去保证
- DedupeResponseHeader,不会去判断response是否已经完成
为此,可以自定义DedupeResponseHeader,加上对response的完成状态进行判断的逻辑。如下,整个逻辑完全照抄DedupeResponseHeaderGatewayFilterFactory,连Config和Strategy都沿用,这样能够使得对它的使用上完全和DedupeResponseHeader一致。
1 |
|
配置
1 | spring.cloud.gateway.default-filters[0]=CustomDedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials |