发表日期:2019年8月15日
跨域警告的发生
如果你做了一些前后端分离的项目,由于此时前端所在的服务地址与后端所在的服务地址不一样,你可能会遇到一个请求被浏览器拦截了的问题,浏览器在检测到当前页面发起的请求不属于当前域就会将其拦截,这是因为浏览器的“同源策略”。
那么,什么是同源策略呢?
同源策略用于限制页面发起不同域(源)的请求,用于提高请求的安全性。
如果两个页面的协议、端口、IP地址(域名)都相同的话,那么这两个页面就是同源,也就是同一个域。举例:
以http://192.168.10.1:8080/index.html为对照源,
http://192.168.10.1:8080/auth/login.html与它是同源;
http://192.168.10.1:8181/index.html与它不是同源,因为端口不一样;
http://192.168.10.30:8080/index.html与它不是同源,因为IP地址不一样。
有些人会问,既然域不一样就会拦截,为什么我用了xxxCDN的css文件,这个请求没有被拦截呢?
这里要提一些并不是所有的跨域请求都会被拦截的。
1.通常浏览器不会拦截一些跨域资源嵌入的请求。
这种所谓的资源嵌入,就是类似于<img>
标签中的src,<script>
中的src,<link>
中的href这样的请求,这样的请求是直接请求资源嵌入到你的页面中的。所以你使用某个cdn的css文件不会被拦截。【所以有种方式就是通过这种嵌入的方式来进行解决跨域的问题】
2.通常浏览器不会拦截一些跨域写资源的请求。
这种所谓的跨域写资源,就是所谓的超链接请求,页面重定向,非XMLHttpRequest方式的表单提交(普通的form表单提交)等等。
3.通常浏览器会拦截跨域读资源的请求。
XMLHttpRequest提交表单,XMLHttpRequest请求资源,【常见于异步操作】除此之外,要再次强调的是,同源策略是浏览器的安全策略。所以如果你直接通过postman这些能够不借助浏览器来发http请求的软件来发请求的话,它是不会拦截你的跨域请求的。
一个可以用于测试的例子:
(当我把下面的html文件部署到一个web服务器(tomcat,apache等)中的时候,此时这个网页处于的域应该是我的本机地址localhost:80
,此时我根据上述的三个方面来测试浏览器是否拦截。结果是只有最后一个是被拦截的。【不要不部署就直接打开这个页面,否则的话它只是一个普通的本地文件,而非网络文件,此时浏览器不会认为这是一个域】)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用于测试跨域</title>
</head>
<body>
<!-- 跨域资源嵌入,允许 -->
<img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif" alt="">
<!-- 跨域资源写操作,允许 -->
<a href="http://www.baidu.com">百度</a>
<form action="http://www.baidu.com" method="post" >
用户名:<input type="text" name="username" value="" placeholder="">
<input type="submit" name="提交" value="提交">
</form>
<!-- 跨域资源读操作,禁止 -->
<button onclick="senddata()">XMLHttpRequest请求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
xmlhttp.open("GET","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif",true);
xmlhttp.send();
</script>
</body>
</html>
补充:
为什么浏览器会使用同源策略?它想解决什么问题?
首先,先谈一下cookie吧,cookie主要用于存储一些当前网站的一些数据,在一些旧的web开发中有的还会把用户登录信息存储到cookie中。那么,从安全的角度来考虑的话,你应该希望你的网站的cookie不能被另外一个网站使用(不然cookie中的数据就非常容易被别人窃取了),所以这就引入了域的概念,通过域来限制资源的使用,拦截跨域的资源请求。
如何允许跨域
有很多手段来解决跨域,但常见的用于解决跨域调用接口的问题就是CORS
CORS
- 如何允许跨域,一种解决方法就是目的域告诉请求者允许什么来源域来请求,那么浏览器就会知道B域是否允许A域发起请求。
- CORS("跨域资源共享"(Cross-origin resource sharing))就是这样一种解决手段。
CORS使得浏览器在向目的域发起请求之前先发起一个OPTIONS方式的请求到目的域获取目的域的信息,比如获取目的域允许什么域来请求的信息。
此时目的域通常需要在响应头中添加以下信息:
- Access-Control-Allow-Origin:用来声明什么域可以向当前域发起请求。
- Access-Control-Allow-Methods:用来声明可以向当前域发起什么类型的请求。
- Access-Control-Max-Age:用来指定本次OPTIONS请求的有效期,单位为秒,在此期间不用发出另一条OPTIONS请求。
- Access-Control-Allow-Headers:用来允许你附加什么特殊的请求头来发起请求。【有些前后端分离项目会把token放到header中,这时候这个请求头就需要Access-Control-Allow-Headers来声明了】
【在OPTIONS请求成功后,浏览器会把这些信息记录下来,用来判断发往目的域的请求是否需要拦截。如果OPTIONS请求失败,那么原本要发起的请求就不会发送。】
但有时候发请求是不会触发OPTIONS请求的。如果这个请求符合以下条件的话:
1.请求的方式是GET、POST或HEAD。
2.请求头属于Accept,Accept-Language,Content-Language,Content-Type ,Viewport-Width。
3.请求头中Content-Type属于application/x-www-form-urlencoded、multipart/form-data、text/plain中的一个。这种请求也被称为“简单请求”。
简单请求的测试:
下面的例子可以用于测试简单和非简单请求是否会发OPTIONS请求
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用于测试简单请求</title>
</head>
<body>
<button onclick="senddata()">XMLHttpRequest请求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
xmlhttp.open("GET","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1565884598468&di=bb6ccc7a3280b183e4e13c852bc8353c&imgtype=0&src=http%3A%2F%2Fs04.lmbang.com%2FM00%2FCA%2FDE%2FDpgiA1uPAOKAXmJfAADir1hWa-A750.gif",true);
xmlhttp.send();
//如果你有一个可以用来测试的接口,可以尝试把这一段注释了,来测试是否发送OPTIONS请求。
// xmlhttp.open("POST","http://localhost:8080/hello",true);
//下面通过加了一个请求头,使得这个请求不是一个不发OPTIONS的请求。
// xmlhttp.setRequestHeader("Content-Type", "application/json")
// xmlhttp.send();
}
</script>
</body>
</html>
后端的处理
下面基于Spring MVC框架来说明后端如何返回返回CORS响应头(注:在spring mvc中,你可以直接使用@CrossOrigin
来简单返回CORS响应头。)。
前端请求测试代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>用于测试OPTIONS</title>
</head>
<body>
<!-- 跨域资源读操作,禁止 -->
<button onclick="senddata()">XMLHttpRequest请求</button>
<script type="text/javascript">
var xmlhttp=new XMLHttpRequest();
function senddata(){
//如果你有一个可以用来测试的接口,可以尝试把这一段注释了,来测试是否发送OPTIONS请求。
xmlhttp.open("POST","http://localhost:8080/hello",true);
//下面通过加了一个请求头,使得这个请求不是一个不发OPTIONS的请求。
xmlhttp.setRequestHeader("Content-Type", "application/json")
xmlhttp.send();
}
</script>
</body>
</html>
当我们没有部署接口的时候,我们就可以看到,页面是发了一个OPTIONS请求的:
而且,浏览器控制台也提示以下信息:
当我们部署了接口的时候,我们先尝试不返回正规的CORS响应头。
由于没有返回CORS响应头,所以OPTIONS没有请求到合适的CORS信息,所以请求就会被拦截,所以就会报下图的两个警告。
当我们在响应中添加CORS响应头后,我们可以看到我们刚刚设置的CORS响应头被OPTIONS请求成功了。
而且请求也被发出去了:
对于不同编程语言的如何使用CORS,可以自查。
补充:
- 除了正常的,例如通过js来处理跨域的。还有一些沙雕的手法,通过修改浏览器来不进行同源策略就是其中一种(治标不治本)。