跨域解决方案之CORS及其相关概念

在讲解 CORS 之前先了解以下几个概念

同源策略

同源策略(Same origin policy)是一种约定,是浏览器限制一个域名与另外一个域名的资源的交互的规则,是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

如果一个请求地址里面的协议、域名和端口号都相同,就属于同源

举个例子,判断下面URL是否和http://www.a.com/a/a.html同源:

  • http://www.a.com/b/b.html 同源

  • http://www.b.com/a/a.html 不同源,域名不相同

  • https://www.a.com/b/b.html 不同源,协议不相同

  • http://www.a.com:8080/b/b.html 不同源,端口号不相同

举个例子:当一个浏览器的两个 tab 页中分别打开百度和谷歌的页面,当浏览器的百度 tab 页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

同源策略是浏览器的行为,是为了保护本地数据不被 JavaScript 代码获取回来的数据所污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。

也就是说,不允许跨域访问(同源策略)并非是浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。

有些浏览器不允许从 HTTPS 跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是特例。

假如没有同源策略会如何?

我们来设想一下,有一个域名为www.a.com的 A 网站,在该网站里面放置一个域名为www.b.com的 B 网站的iframe,并且该 iframe 100% 宽度高度。

  • iframe 是 HTML 的一个嵌入式框架,每个框架里面可以加载一个网页。比如一个网页划分了几个栏目,都要在主页显示,每个栏目占用一个框架。也就是每个栏目做成了一个小网页。这样在打开主页时各栏目可以同时分别加载。

用户在不知道域名的情况下访问了 A 网站,但他以为是访问了 B 网站,所以正常登录了 B 网站。

这时 A 网站通过 document.querySelector(‘iframe’).contentWindow.document.cookie 脚本就能获得该用户在 B 网站下的 cookie,并且可以随意冒充该用户了。

是不是很危险??但是有了同源策略的存在,当 A 网站试图获取 B 网站的 cookie 的时候,浏览器会提示一个错误。

Uncaught DOMException: Blocked a frame with origin "fiddle.jshell.net" from accessing a cross-origin frame.

同理除了不能获取 cookie,同源策略也限制了 DOM 节点的访问,因为与其获取 cookie,倒不如直接监控 input 输入框,获取用户的账号密码来得更简单。

什么是跨域 ?

为了保证浏览器的安全,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。这叫作同源策略,同源策略是浏览器安全的基石。

依据浏览器同源策略,非同源脚本不可操作其他源下面的对象,想要操作其他源下的对象就需要跨域。综上所述,在同源策略的限制下,非同源的网站之间不能发送 AJAX 请求。如有需要,可通过降域或其他技术实现。

跨站请求伪造

Web 常见攻击手段有:

  1. XSS 攻击
  2. CSRF攻击(Cross-site request forgery 跨站请求伪造,同源策略主要防范此类攻击)
  3. SQL 注入
  4. DDOS 攻击
  5. SYN 攻击

这里重点讲解一下CSRF 攻击

百度的解释是:

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。如下面的这个例子:

假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName,那么,一个恶意攻击者可以在另一个网站上放置如下代码: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">,如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。

透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作

防御措施:

  • 检查 Referer 字段。HTTP 头中有一个 Referer 字段,该字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer 字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
  • 添加校验 token。由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行 CSRF 攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。

为什么要跨域 ?

在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域访问问题。在请求的过程中我们要想获取数据一般都是 post/get 请求,所以跨域问题出现。

跨域问题来源于 JavaScript 的同源策略,即只有协议+主机名+端口号相同,则允许相互访问。也就是说 JavaScript 只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。跨域问题是针对 JS 和 AJAX 的,HTML 本身没有跨域问题,比如<a> 标签、<script> 标签、甚至 <form> 标签(可以直接跨域发送数据并接收数据)等。

跨域的方法

克服跨域限制的方法有:

  • 通过 JSONP 跨域
  • 通过修改 document.domain 来跨子域
  • 使用 window.name 来进行跨域
  • 使用 HTML5 中新引进的 window.postMessage 方法实现跨域来传送数据
  • 使用代理服务器,使用代理方式跨域更加直接,因为同源限制是浏览器实现的。如果请求不是从浏览器发起的,就不存在跨域问题了。

跨域解决方案之 CORS 技术

为了解决浏览器跨域问题,W3C 提出了跨源资源共享方案,即 CORS(Cross-Origin Resource Sharing)。CORS 可以在不破坏即有规则的情况下,通过后端服务器实现 CORS 接口,从而实现跨域通信。

通俗地讲,它允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了AJAX只能同源使用的限制。

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS 的通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求(预检请求),但用户不会察觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

CORS 将请求分为两类:简单请求(simple request)非简单请求(not-so-simple request),分别对跨域通信提供了支持。

简单请求

只要同时满足以下两大条件,就属于简单请求。

① 请求方法是以下三种方法之一(发送 HTTP 请求时在头信息中不能包含任何自定义字段):

  • HEAD
  • GET
  • POST

② HTTP 信息不超出以下几个字段:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type(仅限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

一个简单请求的例子:

GET /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, sdch, br
Origin: http://www.test.com
Host: www.test.com

对于简单请求,CORS 的策略是请求时在请求头中增加一个 Origin 字段(浏览器发现这次跨源 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin字段),上面的 Origin 字段用来说明本次请求来自哪个源(协议 + 域名 + 端口),服务器收到请求后,根据该字段判断是否允许该请求访问。

  • 如果不允许,就不在返回的 HTTP 头信息中添加 Access-Control-Allow-Origin 字段。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequestonerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。
  • 如果允许,就在返回的 HTTP 头信息中添加 Access-Control-Allow-Origin字段,并返回正确的结果。该字段是必须的,它的值要么是请求时 Origin 字段的值,要么是一个 * ,表示接受任意域名的请求。

除了上面提到的 Access-Control-Allow-Origin,还有几个字段用于描述 CORS 返回结果:

  • Access-Control-Allow-Credentials:可选,值为布尔类型,表示用户是否可以发送、处理 Cookie。默认情况下,Cookie 不包含在 CORS 请求中,设为 true 表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。且这个值也只能设为true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
  • Access-Control-Expose-Headers:可选,可以让用户拿到的字段。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段(这几个字段无论设置与否都可以拿到的):Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

非简单请求

凡是不同时满足上面两个条件,就属于非简单请求。

对于非简单请求的跨源请求,浏览器会在真实请求发出前增加一次 OPTION 请求,称为预检请求(preflight request)。预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作。

例如一个 GET 请求:

OPTIONS /test HTTP/1.1
Origin: www.test.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-Custom-Header
Host: www.test.com

与 CORS 相关的字段有:

  • 请求使用的 HTTP 方法 Access-Control-Request-Method
  • 请求中包含的自定义头字段 Access-Control-Request-Headers

服务器收到请求时,需要分别对 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 进行验证,验证通过后,会在返回的 HTTP 头信息中添加:

Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

它们的含义分别是:

  • Access-Control-Allow-Methods:真实请求允许的方法。
  • Access-Control-Allow-Headers:服务器允许使用的字段。
  • Access-Control-Allow-Credentials:是否允许用户发送、处理 cookie。
  • Access-Control-Max-Age:预检请求的有效期,单位为秒。有效期内,不会重复发送预检请求。

当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。

非简单请求为何需要预检请求 ?

「预检请求」要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。

此外,要回答某个请求是否接受跨源,可能涉及额外的计算逻辑。这个逻辑可能很简单,比如一律放行。也可能比较复杂,结果可能取决于哪个资源哪种操作来自哪个 origin。对浏览器来说,就是某个资源是否允许跨源这么简单;对服务器来说,计算成本却可大可小。所以我们希望最好不用每次请求都让服务器劳神计算。这就好比,你不知道啥时候开学,先打电话问问,如果开学了,就带着行李去上学,而不是不管三七二十一,直接背着行李去学校。

CORS 实现

CORS 的代码实现比较简单,主要是要理解 CORS 实现跨域的原理和方式。在项目的 config 包下新建一个 CORS 配置类,实现 WebMvcConfigurer 接口。

WebMvcConfigurer.java

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  // 允许跨域访问的路径
        .allowedOrigins("*")        // 允许跨域访问的源
        .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")  // 允许请求方法
        .allowedHeaders("*")        // 允许头部设置
        .allowCredentials(true);    // 是否允许用户发送、处理cookies
    }
}

这样,每当客户端发送请求的时候,都会在头部附上跨域信息,就可以支持跨域访问了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值