最近在做前后端交互的时候遇到了特殊的跨域问题。其实自己平时做项目的时候也多多少少处理过跨域问题,但是这次情况特殊。正好借此机会将跨域问题的解决方法做一下总结,以备不时之需。
情景#
自己前段时间在学习前端,所以想趁着这个机会自己搞个小工具玩玩。所以就有了如下这个极其简陋的弹幕网站。
为了实现弹幕的持久保存,自己尝试前后端交互将弹幕存储在数据库中,然后前端每间隔一段时间就从后台拉取一次数据。
实现其实比较简单,但是当我在本地开始测试向后台发送请求的时候,问题出现了:
很明显这是一个跨域问题,所以接下来我们就要动用我们所学的知识去解决它。
首先第一步,分析问题原因:什么是跨域?
什么是跨域?#
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源
跨域其实不是什么 bug,而是由于浏览器为了安全起见指定了一系列 “同源策略”,这些同源策略可以保证用户信息的安全,防止被恶意的网站窃取数据。
该政策限制网页的某些行为必须限制在与自己 “同源” 的网页中才能进行,如果不是同源,就会导致该行为无法生效,也就是跨域失败。
这里出现了两个概念,一个是同源,一个是跨域行为
同源的定义包含三个方面
- 协议相同
- 域名相同
- 端口相同
只有三个条件都满足,才能认定两个网页是” 同源 “
跨域行为(自己定义的名词,大概就是我们会被同源政策影响到的操作)随着互联网的发展,范围变得越来越宽泛,一般我们常见的跨域行为包括
- 获取 Cookie、LocalStorage 和 IndexDB
- 获得 DOM 和 JS 对象
- AJAX 请求
我们可以看到在前后端交互中最常见的 AJAX 请求也赫然在列,在前后端交互中解决跨域问题的不可避免的。
那为什么要定义同源策略呢?没有跨域限制不是更好吗?
如果没有跨域限制,网页将很容易受到 XSS、CSRF 等攻击,因为没有限制,恶意网站同样可以自由地发起攻击,这将大大提高网站的维护成本。因此,同源策略其实是一把双刃剑,只是在保护网页的同时,偶尔总会误伤友军。
知道了问题的根源,我们就可以对症下药,寻找解决方案
解决方案#
我们可以看到,跨域问题的关键在于我们的 M请求处于限制范围内,没有做到同源>,从而导致的。
重点已经标出来了,其实我们解决的方法也就是从这两个思路着手
- 采用不在同源策略的行为操作
- 想办法让行为处于同源状态
这里我们指针对 AJAX 请求,对于其他诸如 cookie、iframe 等的跨域方案,其实参考相关的博客相信一定能得到答案
1.JSONP 跨域#
我们可以通过在 AJAX 请求中定义 JSONP 类型实现跨域,虽说如此,但 JSONP 本质上采用的是和 AJAX 完全不同的请求方式。
传统的 AJAX 请求其实是xhr
的异步请求,而 JSONP 本质上是去构建一个<script>
标签,利用script
标签中的src
不受同源政策的限制,在src
中填写后端URL
并添加回调函数,获取到的数据就通过回调函数处理。
参考阮一峰的博客, 实现思想大致如此:
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('response data: ' + JSON.stringify(data));
};
由于
<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo
函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse
的步骤。
如何在 AJAX 实现:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
data: {}
});
如何在 vue 上实现:
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
从实现原理上可以看出 JSONP 还是存在弊端,那就是使用 JSONP 必须是 GET 请求,如果要 POST 请求实现跨域,还是需要使用其他方法
2.WebSocket#
websocket
本身就是一种通信协议,通过websocket
通信,实际上就可以跨过同源策略, 实现某种意义上的 "同源"。
下面是websocket
请求的 HTTP 头信息,重点关注Origin
字段,这是实现跨域的关键
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
Origin 字段表示该请求的请求源,只要 Origin 字段中的源域名和请求的目的域名是同一个,就可以通过同源策略中的域名一致,实现跨域。
如果允许通信,WebSocket 的响应头如下
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
3.CORS#
CORS 即跨域资源共享 "(Cross-origin resource sharing),它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
这是解决跨域问题的常用方法。
实现原理#
其实现原理如图
CORS 请求主要分成两类:简单请求和非简单请求。
满足以下条件的就是简单请求,否则就是非简单请求
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP 的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
简单 CORS 请求只是在请求的时候在 http 头中加入 Origin 字段
非简单 CORS 请求的话,浏览器会在正式通信后先发送预检请求,先询问服务器是否允许请求,得到响应,检查相关字段后就可以做出回应,发起正式请求
假设现在发起一段 js 脚本
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
其中预检请求的请求方法是 OPTIONS,具体请求头类似如下
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
主要关注三个字段
-
Origin
表示请求来自哪个源
-
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是
PUT
。 -
Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是
X-Custom-Header
。
得到响应如下之后就能确认允许跨域请求
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
其中
//表示支持任意跨域请求
Access-Control-Allow-Origin: *
//表示支持跨域请求的方法
Access-Control-Allow-Methods: GET, POST, PUT
//当浏览器请求包含Access-Control-Request-Headers的时候必需,表示支持的头信息字段
Access-Control-Allow-Headers: X-Custom-Header
//允许发送cookie和认证信息
Access-Control-Allow-Credentials: true
//指定本次预检请求的有效期
Access-Control-Max-Age: 1728000
实现方式#
这里主要是后端的操作,这里用了java
springboot
的跨域方式做为样例
@Configuration
public class CORSConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")
.maxAge(3600);
}
};
}
}
其实就是在服务端响应的时候添加请求头,springboot
还支持在不同controller
上使用注解添加
4.nginx 代理跨域#
这个原理也简单,其实就是让前端和后端处于同源上,利用 nginx 的反向代理可以修改请求的域名、端口,也能添加 cookie 信息啥的实现跨域
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
另外使用一些中间件的代理方式其原理都是这回事,这里就不加赘述
分析问题#
一通分析#
上面解决方案说了一大堆,但最终还是要回归我们的问题,这次,我们开始对症下药。首先再看一遍报错日志:
嗯?好像和想象中的不太一样,常见的跨域问题应该如同:
这种,看起来其中有诈?
果不其然,通过后台添加跨域设置,我们的报错信息依然没有变化。
这时我们就需要对报错信息好好分析(其实这应该是分析日志的第一步,为了强行引入跨域解决方案,因此特地将分析放在了后面)
这句话引起了我的注意
Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
通过查阅资料发现,原来这里我的前后端交互都是再本地实现,本地打开 html 使用的 file 协议,但是 file 协议的请求无法被浏览器认可,网上提供的方法如下
在谷歌浏览器下的快捷方式位置
在目标处添加:
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -args --disable-web-security --user-data-dir --allow-file-access-from-files
大概就是这样,但是我还是不推荐使用这种方法,因为这样的方式并不是特别优雅的解决方法
另外的尝试#
另外的解决方法就是在本地部署 nginx,诸如上面提到过的解决方法,不通过 file 协议打开文件。
什么是 file 协议的打开方式?
大概就是这种
要换成用 http 形式打开的方式,诸如这种
还有如果不嫌麻烦直接把网页部署到服务器上也是一种解决方法
意外的结果#
但是!!最终问题还是没有得到解决!这可把我难到了。。
事必有因,经过一个多小时的不懈努力,我终于找到了问题的根源
————
ajax 的请求 URL 必须以 http 的格式。。。。
啊啊啊啊果然还是我太菜了。。
因为刚学 ajax,对其原理不熟,导致最后出现了这种问题。