跨域和OPTIONS这对欢喜冤家
简介
相信做过前端开发的同事,包括做小程序或者小游戏的码友们应该都看过类似下面的错误,这个错误是由于 JavaScript 代码向服务器发送了 HTTP 请求引起的。
1 | Access to XMLHttpRequest at 'http://www.xxx.com/yyy' from origin 'null' has been blocked by CORS policy: |
如果是第一次遇到,你肯定会觉得很好奇,忍不住会去一探究竟~
还有些同事会遇到另一个神奇的错误,即发送GET或者POST请求之前,居然先给服务器发送了一个 OPTIONS 请求,让人不可思议的是这个 OPTIONS 请求是自动发的,服务器在没有任何设置的条件下直接将这个请求夭折掉,如下返回 403
错误,也可能是其他错误。
1 | OPTIONS http://www.xxx.com/yyy 403 |
引起这些问题的罪魁祸首就是 跨域
,今天我跟大家一起以实际的例子来看看这个神奇的 跨域
问题。
文中使用的代码都可以在 Github 找到,大家根据需要自行采纳。
同源策略
域,是指由 协议
+ 域名
+ 端口号
组成的一个虚拟概念。
如果两个域的 协议
、域名
、端口号
都一样,就称他们为同域,但是只要三者之中有一个不一样,就不是同域。
那么 跨域请求
简单来说,就是在一个域内请求了另一个域的资源,由于域不一致会有安全隐患如 CSRF
(Cross-site request forgery)攻击。
在百度百科里面是这样定义 同源策略
的,如下:
1 | 同源策略(SOP,Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。 |
听起来,这玩意挺高大上的,简单理解 同源策略
就是一种安全策略,为了安全而生的一种限制措施。它是由 Netscape 提出的一个著名的安全策略,现在所有支持 JavaScript 的浏览器都会使用这个策略,也是必须遵守的一个策略。
那么 同源策略
中的 同源
是指 域名
,协议
,端口
三者必须相同,如果有任何一个不同就会引起跨域。
下表给出了相对 http://a.xx.com/yy/zz.html
同源检测的示例:
URL | 结果 | 原因 |
---|---|---|
http://a.xxx.com/ff/other.html |
成功 | 域名、协议、端口(默认80)一致 |
http://a.xxx.com/gg/hh/another.html |
成功 | 域名、协议、端口(默认80)一致 |
https://a.xxx.com/secure.html |
失败 | 不同协议 ( HTTPS和HTTP ) |
http://a.xxx.com:81/dir/etc.html |
失败 | 不同端口 ( 81和80) |
http://a.wpq.com/yy/other.html |
失败 | 不同域名 ( xxx和wpq) |
http://123.21.122.12/dir |
失败 | 域名IP不等同于域名 |
http://xx.xxx.com/dir2/ |
失败 | 主域相同,子域不同 |
简单来说,HTML 代码运行在一个web主机上面(假设域名是 http://a.xx.com/yy/zz.html
),而HTML代码中有需要请求服务器某 API 接口(http://api.user.com/name
)的,那么就会造成跨域问题。
同源策略会影响:
(1) Cookie、LocalStorage 和 IndexDB 无法读取;
(2) DOM 无法获得;
(3) AJAX 请求不能正常发送,有可能还会引起 OPTIONS 请求;
OPTIONS请求
大家所熟知的HTTP请求最多的应该就是 GET 和 POST 请求,这两种请求也是软件开发中用的最多的。
GET:向特定的资源发出请求,一般对服务器来说是一个只读的请求,不会对资源进行写操作。
POST:向指定资源提交数据进行处理请求,例如提交表单或者上传文件,数据被包含在请求体(body)中,该请求可能会对服务器资源进行读写操作。
除了这两种请求外,HTTP还有其他种类的请求,如下:
PUT:向指定资源位置上传其最新内容,一般用于资源的整体更新,而下面的 PATCH 用于资源的部分更新。
DELETE:请求服务器删除所标识的资源。
HEAD:向服务器索要与 GET 请求相一致的响应,只不过响应体将不会被返回,可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
TRACE:回显服务器收到的请求,主要用于测试或诊断。
OPTIONS:返回服务器针对特定资源所支持的 HTTP 请求方法。也可以利用向Web服务器发送 ‘*’ 的请求来测试服务器的功能性。该请求不会修改服务器资源,相对比较安全。
CONNECT:是
HTTP/1.1
协议预留的,能够将连接改为管道方式的代理服务器。通常用于 SSL 加密服务器的链接与非加密的 HTTP 代理服务器的通信。PATCH:是对 PUT 方法的补充,用来对已知资源进行局部更新。当资源不存在时,PATCH 会创建一个新的资源,而 PUT 只会对已存在的资源进行更新。
其中 GET, POST 和 HEAD 方法是 HTTP1.0 定义的三种请求方法,在 HTTP1.1 又新增了六种请求方法,即 OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。如果想了解更多 HTTP 历史的朋友,可以阅读我之前的写的一篇文章 HTTP 演进史,嘿哈🙋。
再说一下 OPTIONS
请求,该请求与 HEAD
请求有点类似,一般也是用于客户端查看服务器的性能。
OPTIONS
方法会请求服务器返回该资源所支持的所有HTTP请求方法,该方法会用来代替资源名称,向服务器发送 OPTIONS
请求,可以测试服务器功能是否正常。JavaScript 的 XMLHttpRequest 对象进行CORS
跨域资源共享时,就是使用 OPTIONS
方法发送嗅探请求,以判断是否有对指定资源的访问权限。
那么需要满足哪些条件才会触发 OPTINS
请求呢?
实例验证
在没有回答上面的问题之前,我们还是来做个实验吧~
你需要将 Chrome 浏览器的审查视图打开,最好把 Disable Cache
也勾选上禁止 Chrome 使用网络缓存,这样才不会影响下面的实验。
下面是 Springboot
关于登录的一个示例代码,如下:
1 | import org.springframework.web.bind.annotation.*; |
你大可不必去了解这个代码的具体逻辑,现在你只需要知道他是用来给 JavaScript 调用的一个登录API即可。
再来一个 HTML 文件,模拟请求登录的API,请求 HTTP 使用 Ajax,代码如下:
1 | <script> |
使用 Chrome 浏览器直接打开这个HTML文件即可,然后启动 Java 服务,在浏览器中点击按钮进行 GET 请求。
此时请求会报下面的错误:
1 | Access to XMLHttpRequest at 'http://localhost:8080/signin/name?username=jack&userpwd=123' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. |
确实是造成了跨域请求,导致请求失败。
但是令人遗憾的是并没有看到发出 OPTIONS 请求,使用 Fiddler 抓包,可以看到只有 GET 请求,如图所示:
难道是自己写代码的姿势不对吗?!
其实,在 HTML 中使用 HTTP 请求,发生 OPTIONS 请求是需要几个条件的:
- 1、必须是跨域请求
- 2、自定义了请求头
- 3、请求头中的
content-type
是application/x-www-form-urlencoded
,multipart/form-data
,text/plain
之外的格式
满足1和2或者满足1和3就会发生 OPTIONS 请求,首先我们确定了上面的示例是跨域请求,但是不满足后面的两个条件之一。
我们修改一下HTML代码增加一个 content-type
,如下:
1 | <script> |
此时在浏览器中(需要使用 Chrome 的审查视图)可以看到报错信息:
1 | OPTIONS http://localhost:8080/signin/name?username=jack&userpwd=123 403 |
抓包工具中也可以看到发生了 OPTIONS 请求,如下图:
也可以自定义 Header 头来进行验证,代码如下:
1 | <script> |
验证结果和上面一致,也会发生 OPTIONS 请求。
再聊OPTIONS
在 RFC2616-HTTP/1.1 中关于 OPTIONS 有详细的描述,感兴趣的可以看一下 9.2 OPTIONS
小节。
OPTIONS 请求方法的主要用途有两个:
1、获取服务器支持的 HTTP 请求方法;
2、用来检查服务器的性能,如上面例子中的AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个 HTTP OPTIONS 请求头,用以判断实际发送的请求是否安全;
HTT P的 OPTIONS 请求,有很多地方也被称之为预请求或者预检请求,换句话说就是试探性的请求不算是正式请求。
为了避免对服务器产生一些副作用,类似上面例子中的网页中的请求就会产生 OPTIONS 请求,也算是一种对服务器的保护。只有当服务器允许后,浏览器才会发出正式的请求,否则不发送正式请求。
我们可以使用 curl
模拟 OPTIONS 请求,例如下面请求谷歌:
1 | curl -i -v -X OPTIONS https://www.google.com |
可以看到请求的响应情况:
1 | < HTTP/2 405 |
SpringBoot 解决跨域
话说,同源策略引起了跨域问题,本身是为了安全起见为何我们还要去解决这个问题呢?这是因为 Web 前端是我们自己开发的,也就是说我们是知道自己的 Web 请求是安全的(类似于白名单客户),就需要让它顺利访问后端服务,所以解决这个跨域问题势在必行。
解决跨越的问题,在网上有很多的路子,目前大概有下面几种解决方案,如下:
- JSONP
- 简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。但它仅支持GET方法不支持POST等其他请求方法,而且可能会遭受XSS攻击。
- CORS
- postMessage
- websocket
- Node 中间件代理
- Nginx 反向代理
- window.name+iframe
- location.hash+iframe
- document.domain+iframe
今天我们使用 SpringBoot 自带的注解来解决这个问题😃。
在说解决方案之前,还是先了解一下 CORS(Cross-origin resource sharing),其全称是”跨域资源共享”,是 W3C 的一个标准。
CORS 允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了AJAX只能同源使用的限制。
CORS 需要浏览器和服务器同时支持。幸运的是目前几乎所有的浏览器都支持该功能,唯一美中不足的是IE浏览器的版本不能低于IE10。
实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。SpringBoot 自带注解 CrossOrigin
可以用来解决跨域问题。
修改一下 Controller 的代码,增加 CrossOrigin
注解,示例代码如下:
1 |
|
重新启动服务,抓包工具可以看到 OPTIONS 和 GET 请求都正常执行,返回码都是200。
可以针对某个方法添加 CrossOrigin
注解,也可以对整个 Controller 添加该注解。
关于 CrossOrigin
注解,大家可以自行实践,这里不再赘述。
一直坚持在学习的路上努力~