深入了解跨域问题

一、什么是跨域?

跨源资源共享 (CORS)(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。
跨源域资源共享(CORS)机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

1、同源策略及其限制内容

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
在这里插入图片描述
同源策略限制内容有:
● Cookie、LocalStorage、IndexedDB 、SessionStorage 等存储性内容
● DOM 节点
● HTTP接口(例如 XMLHttpRequest 或 Fetch)等

但是有三个标签是允许跨域加载资源:


2、常见跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:
在这里插入图片描述

3、跨域访问的规范标准

跨源资源共享标准新增了一组 HTTP 头部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型 的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证 相关数据)。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
接下来的内容将讨论相关场景,并剖析该机制所涉及的 HTTP 头部字段。

4、访问场景

这里,我们使用三个场景来解释跨源资源共享机制的工作原理。这些例子都使用 XMLHttpRequest 对象。
简单请求
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”。若请求 满足所有下述条件,则该请求可视为“简单请求”:
● 使用下列方法之一:
○ GET
○ HEAD
○ POST
● 除了被用户代理自动设置的头字段(例如 Connection,User-Agent或其他在 Fetch 规范中定义为 禁用头部名称 的头部),允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的头部字段集合。该集合为:
○ Accept
○ Accept-Language
○ Content-Language
○ Content-Type(需要注意额外的限制,限于下列三者之一)
■ text/plain
■ multipart/form-data
■ application/x-www-form-urlencoded
● 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
● 请求中没有使用 ReadableStream 对象。
比如说,假如站点 https://foo.example 的网页应用想要访问 https://bar.other 的资源。foo.example 的网页中可能包含类似于下面的 JavaScript 代码:

const xhr = new XMLHttpRequest(); const url = 'https://bar.other/resources/public-data/'; xhr.open('GET', url); xhr.onreadystatechange = someHandler; xhr.send();
const xhr = new XMLHttpRequest(); const url = 'https://bar.other/resources/public-data/'; xhr.open('GET', url); xhr.onreadystatechange = someHandler; xhr.send();

客户端和服务器之间使用 CORS 头部字段来处理权限:
以下是浏览器发送给服务器的请求报文:
请求头部字段 Origin 表明该请求来源于 http://foo.example。
本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被 任意 外域访问。
使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。如果服务端仅允许来自 https://foo.example 的访问,该头部字段的内容如下:
备注: 当响应的是附带身份凭证的请求时,服务端必须明确 Access-Control-Allow-Origin 的值,而不能使用通配符“*”。
预检请求
与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
如下是一个需要执行预检请求的 HTTP 请求:
上面的代码使用 POST 发送一个请求,该请求包含了一个自定义的请求头部字段X-Healthlink: test。另外,该请求的 Content-Type 为 application/json。因此,该请求需要首先发起“预检请求”。
在这里插入图片描述

备注: 如下所述,实际的 POST 请求不会携带 Access-Control-Request-* 头部,它们仅用于 OPTIONS 请求。
下面是服务端和客户端完整的信息交互。首次交互是 预检请求/响应:
浏览器检测到,从 JavaScript 中发起的请求需要被预检。从上面的报文中,我们看到,第 1~10 行发送了一个使用 OPTIONS 方法 的“预检请求”。OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。预检请求中同时携带了下面两个头部字段:
头部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。头部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求头部字段:X-Healthlink 与 Content-Type。服务器据此决定,该实际请求是否被允许。
服务器的响应携带了 Access-Control-Allow-Origin: https://foo.example,从而限制请求的源域。同时,携带的 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST 和 GET 方法发起请求(与 Allow 响应头部类似,但其具有严格的访问控制)。
头部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-Healthlink 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。
最后,头部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个 最大有效时间,如果该头部字段的值超过了最大有效时间,将不会生效。
预检请求完成之后,发送实际请求:
附带身份凭证的请求
备注: 当发出跨源请求时,第三方 Cookie 策略仍将适用。无论如何改变本章节中描述的服务器和客户端的设置,该策略都会强制执行。
XMLHttpRequest 或 Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨源请求,浏览器不 发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。
本例中,https://foo.example 的某脚本向 https://bar.other 发起一个 GET 请求,并设置 Cookies:
第 6 行将 XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。因为这是一个简单 GET 请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者。
在这里插入图片描述

客户端与服务器端交互示例如下:
即使第 10 行指定了 Cookie 的相关信息,但是,如果 https://bar.other 的响应中缺失 Access-Control-Allow-Credentials: true(第 17 行),则响应内容不会返回给请求的发起者。
预检请求和凭据
CORS 预检请求不能包含凭据。预检请求的响应必须指定 Access-Control-Allow-Credentials: true 来表明可以携带凭据进行实际的请求。
备注: 一些企业认证服务要求在预检请求时发送 TLS 客户端证书,这违反了 Fetch 的规范。
Firefox 87 允许通过在设置中设定 network.cors_preflight.allow_client_cert 为 true(bug 1511151)来允许这种不规范的行为。基于 chromium 的浏览器目前总是在 CORS 预检请求中发送 TLS 客户端证书(Chrome bug 775438)。
附带身份凭证的请求与通配符
在响应附带身份凭证的请求时:
● 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com。
● 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“
”,而应将其设置为头部名称的列表,如:Access-Control-Allow-Headers: X-Healthlink, Content-Type
● 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET
对于附带身份凭证的请求(通常是 Cookie),服务器不得设置 Access-Control-Allow-Origin 的值为“
”。
这是因为请求的头部中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://example.com,则请求将成功执行。
另外,响应头部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。
第三方 cookies
注意在 CORS 响应中设置的 cookies 适用一般性第三方 cookie 策略。在上面的例子中,页面是在 foo.example 加载,但是第 20 行的 cookie 是被 bar.other 发送的,如果用户设置其浏览器拒绝所有第三方 cookies,那么将不会被保存。
请求中的 cookie(第 10 行)也可能在正常的第三方 cookie 策略下被阻止。因此,强制执行的 cookie 策略可能会使本节描述的内容无效(阻止你发出任何携带凭据的请求)。
Cookie 策略受 SameSite 属性控制。

5、HTTP 响应头部字段

本节列出了规范所定义的响应头部字段。上一小节中,我们已经看到了这些头部字段在实际场景中是如何工作的。
Access-Control-Allow-Origin
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。
例如,下面的字段值将允许来自 https://mozilla.org 的请求:
如果服务端指定了具体的域名而非“*”,那么响应头部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。
Access-Control-Expose-Headers
译者注:在跨源访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。
Access-Control-Expose-Headers 头让服务器把允许浏览器访问的头放入白名单,例如:
这样浏览器就能够通过 getResponseHeader 访问 X-My-Custom-Header 和 X-Another-Custom-Header 响应头了。
Access-Control-Max-Age
Access-Control-Max-Age 头指定了 preflight 请求的结果能够被缓存多久,请参考本文在前面提到的 preflight 例子。
delta-seconds 参数表示 preflight 预检请求的结果在多少秒内有效。
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials 头指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。当用在对 preflight 预检测请求的响应中时,它指定了实际的请求是否可以使用 credentials。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页。
上文已经讨论了附带身份凭证的请求。
Access-Control-Allow-Methods
Access-Control-Allow-Methods 头部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
有关 preflight request 的示例已在上方给出。
Access-Control-Allow-Headers
Access-Control-Allow-Headers 头部字段用于预检请求的响应。其指明了实际请求中允许携带的头部字段。

6、HTTP 请求头部字段

本节列出了可用于发起跨源请求的头部字段。请注意,这些头部字段无须手动设置。当开发者使用 XMLHttpRequest 对象发起跨源请求时,它们已经被设置就绪。
Origin
Origin 头部字段表明预检请求或实际请求的源站。
origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。
备注: 有时候将该字段的值设置为空字符串是有用的,例如,当源站是一个 data URL 时。
注意,在所有访问控制请求(Access control request)中,Origin 头部字段 总是 被发送。
Access-Control-Request-Method
Access-Control-Request-Method 头部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
Access-Control-Request-Headers
Access-Control-Request-Headers 头部字段用于预检请求。其作用是,将实际请求所携带的头部字段告诉服务器。

二、怎么解决跨域问题?

1、传统JSONP

这种方式主要是利用 script 标签的允许跨域的限制,在IE9的以及之前的时代,跨域的规范还没有在浏览器中普及,如果想跨域访问,只能用这种比较奇葩的方式进行处理。在现代浏览器中,跨域的规范已经被很好的支持,此种方式已经不再使用,在此处就不再做详细的介绍。

2、Spring 跨域

@CrossOrigin 注解
Spring中第一种处理跨域的方式是通过@CrossOrigin注解来标记支持跨域,该注解可以添加在方法上,也可以添加在Controller上。当添加在Controller上时,表示Controller中的所有接口都支持跨域,具体配置如下:
@CrossOrigin注解各属性含义如下:
● allowCredentials:浏览器是否应当发送凭证信息,如Cookie。
● allowedHeaders:请求被允许的请求头字段,*表示所有字段。
● exposedHeaders:哪些响应头可以作为相应的一部分暴露出来(注意,这里只可以一一列举,通配符 * 在这里是无效的)。
● maxAge:预检请求的有效期,有效期内不必再次发送预检请求,默认是1800秒
● methods:允许的请求方法,*表示允许的所有方法。
● origins:允许的域,*表示允许所有域。
addCrossMapping自定义mvc配置
@CrossOrigin注解需要添加在不同的Controller上。所以还有一种全局配置方法,就是通过重写WebMvcConfigurerComposite#addCorsMappings方法来实现,具体配置如下:
CorsFilter 过滤器处理
CorsFilter是Spring Web中提供的一个处理跨域的过滤器,开发者也可以通过该过滤器处理跨域。

3、SpringSecurity 跨域

当我们为项目添加了Spring Security依赖后,通过上面Spring的三种方式很可能就失效了。
由于非简单请求都要首先发送一个预检请求(request),而预检请求并不会携带认证信息,所以预检请求就有被Spring Security拦截的可能。因此通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域就会失效。如果使用CorsFilter配置的跨域,只要过滤器优先级高于SpringSecurity过滤器就不会有问题,反之同样会出现问题。
所以引入Spring Security依赖后,需要使用Spring Security 的专门配置文件进行配置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值