同源策略
同源策略,它是由Netscape提出的一个著名的安全策略。
当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面
当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,
即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。
简言之:
很多人以为同源策略是浏览器不让请求发出去、或者后端拒绝返回数据。NO!实际情况是,请求正常发出,后端接口也正常响应,只不过数据到了浏览器后被丢弃了。
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM节点
- AJAX跨域请求的数据
以下情况都属于跨域:
跨域原因说明 | 示例 |
---|---|
域名不同 | www.jd.com 与 www.taobao.com |
域名相同,端口不同 | www.jd.com:8080 与 www.jd.com:8081 |
二级域名不同 | item.jd.com 与 miaosha.jd.com |
协议不同 | HTTP与HTTPS |
简单来说,是否跨域的3个因素为:协议、域名、端口。
需要注意,如果协议、域名、端口都相同,但是请求路径不同,不属于跨域,如:
https://www.jd.com/item
https://www.jd.com/goods
而上面示意图中,在manage.leyou.com
的页面访问api.leyou.com
的接口,由于二级域名不同,导致跨域。从这个角度来看,只要是前后端分离的项目,必然跨域!(即使部署在同一个服务器,前后端项目端口肯定不同)
跨域Demo
随手建一个SpringBoot项目后,把下面的文件拷过去
目录结构如下:
avatar.png是头像。
UserController
@RestController
public class UserController {
@GetMapping(value = "/getUser/{id}")
public User getUser(@PathVariable("id") Long id) {
System.out.println("id:" + id);
User user = new User();
user.setName("bravo");
user.setAge(18);
user.setAddress("wenzhou");
return user;
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS</title>
<script type="text/javascript" src="/jquery/jquery-2.1.3.min.js"></script>
</head>
<body>
<h1>当前网页来自localhost:7070/index.html</h1>
<h3>页面加载时自动发送GET请求: http://localhost:8080/avatar.png</h3>
<img src="http://localhost:8080/avatar.png" width="100" height="100"><br><br>
<h3>点击发送GET请求: http://localhost:8080/getUser/1</h3>
<input type="text" id="result">
<input type="button" onclick="onButtonClick()" value="get_button">
</body>
<script>
function onButtonClick() {
$.get('http://localhost:8080/getUser/1', function (data) {
console.log("data", data);
});
}
</script>
</html>
JQuery
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
刚才提到过,协议、端口、域名都能造成跨域,这里我们演示最简单的跨域:端口不同导致跨域,我会把同一个项目以不同端口启动两次。
第一次通过IDEA启动7070端口:
第二次通过java -jar指定8080端口启动应用(或者可以再配置一个IDEA的启动按钮,更改yml端口后启动即可):
# install
mvn clean install -Dmaven.test.skip=true
# 指定8080端口启动项目
java -jar /Users/kevin/IdeaProjects/springboot-demo/target/springboot-demo-0.0.1-SNAPSHOT.jar --server.port=8080
请求端口为7070的应用,得到index.html页面,随后点击按钮,向端口为8080的应用发起请求
index.html来自端口为7070的应用,它总共向端口为8080的应用发送两次请求:
<img>
标签自动发起请求,获取头像成功- 点击botton发起请求,获取User失败
但botton的GET请求真的失败了吗?其实AJAX已经拿到了数据(状态码200),只不过浏览器拒绝该数据。
回顾一下开头的那张示意图:
本案例中,页面index.html来自localhost:7070,而和AJAX的GET都指向localhost:8080,都跨域了:
<img src="http://localhost:8080/avatar.png"/>
$.get('http://localhost:8080/getUser/1', function (data){...})
但为什么只有AJAX被拒绝了?
因为浏览器允许<img>
跨域。
是的,浏览器遵守同源策略,但是有若干个标签是允许跨域的,比如:
<img src="xxx"/>
<link href="xxx"/>
<script src="xxx"/>
我之前说Jquery脚本可以直接引用外站的,就是这个道理,因为<script>
也不会跨域(所以即使之前不知道什么是跨域,各种引用也没报错)。
现在,我们主要关心如何解决AJAX跨域问题。
解决跨域
解决跨域的方式很多,比如Node中间件代理、Nginx反向代理等等。这里介绍两种最简单的方式:JSONP和CORS。
JSONP
JSONP可以算是利用同源策略的“漏洞”而创造出来的跨域解决方案,主要是利用<script>
标签实现的(因为<script>
请求回来的数据不会被拦截)。JQuery的JSONP在用法上很简单,为了能让部分同学更好地理解它,我们绕一个弯子,通过几次版本迭代最终引出JSONP。
在此之前,有两个前置知识要说明一下。
首先,<script>
虽然可以避免跨域问题,但它的内容一般都是JS脚本,要么直接在<script>
标签中编写JS脚本,要么网络请求JS脚本(文件)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS</title>
<script type="text/javascript" src="/jquery/jquery-2.1.3.min.js"></script>
<!--1.定义printResponse()方法-->
<scrip