跨域和同源策略
什么是跨域
- 当一个页面访问另一个页面时,协议(
protocol)、域名(Domain)以及端口(port)任意一个不同时都是跨域,会受到浏览器同源策略的限制。
-
同源:
http://example.com:8080/和http://example.com:8080/api/data是同源的。
-
跨域:
http://example.com:8080/和https://example.com:8080/是跨域的(不同协议)。http://example.com:8080/和http://api.example.com:8080/是跨域的(不同域名)。http://example.com:8080/和http://example.com:9090/是跨域的(不同端口)。http://example.com:8080/和https://api.anotherdomain.com:9090/是跨域的(不同协议、域名和端口)。
什么是同源策略
同源策略(Same-Origin Policy)是一种浏览器安全机制,旨在防止一个网页的脚本非法访问另一个网页的资源,特别是当这两个网页来自不同的源时。
举个简单的例子:
假设你在浏览一个银行网站 https://bank.example.com,该网站有一些敏感信息,比如账户余额和个人资料。现在有以下几种情况:
- 同源请求:如果你点击了银行网站上的链接或者提交了一个表单,这些操作都是在同一个网站内部进行的,相当于你让自己的家人帮忙做事,浏览器会允许这种交互。
- 跨源请求:如果有一个恶意网站
http://malicious-site.com尝试通过JavaScript代码获取你在银行网站上的个人信息,这就像是一个陌生人试图闯入你家偷东西。为了保护你,浏览器会根据同源策略阻止这个请求,不让恶意网站访问银行网站的数据。\
跨域请求处理
- 简单请求:浏览器会直接发送请求,但如果没有适当的 CORS 头部,JavaScript 将无法读取响应数据。
- 非简单请求:浏览器会在实际请求之前发送预检请求,以确保服务器同意该类型的跨域请求。只有预检成功后,才会发出实际请求。
什么是JSONP?
JSONP(JSON with Padding)是一种非正式的,轻量级的传输协议,它允许网页通过 <script> 标签向不同域名下的服务器发送GET请求,并接收数据作为JavaScript回调函数的参数。其核心思想是利用 <script> 标签src属性不受同源策略限制的特点,动态创建 <script> 元素来加载外部资源,从而绕过浏览器的安全限制。
- JSON with Padding ,填充式JSON,服务端不再只返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
<img src="xxx"><link href="xxx">也不受同源策略的限制,但是只有script可以返回可以执行的脚本
JSONP的工作原理
-
客户端发起请求:
- 在前端页面中,定义一个全局回调函数,用于处理接收到的数据。
- 动态创建一个
<script>标签,并设置其src属性为包含回调函数名的URL。 - 将这个
<script>标签插入到DOM中,触发浏览器加载指定的URL。
-
服务器端响应:
- 服务器接收到请求后,解析出回调函数名,并根据该名称构建一段JavaScript代码。
- 这段代码通常是一个函数调用,传递给回调函数的数据作为参数。
- 服务器将这段JavaScript代码作为响应返回给客户端。
-
客户端执行回调:
- 浏览器下载并执行返回的JavaScript代码,调用之前定义好的全局回调函数。
- 回调函数处理传入的数据,更新DOM或其他操作。
实例说明
后端node代码:
// http 服务启动
// 内置的http模块
// commonjs node早期 es6模块化
const http = require('http');
// 启动http服务
// 基于请求/响应的简单协议
const server = http.createServer((req, res) => {
const users =[{
id: 1,
name: 'zs'
},{
id: 2,
name: 'ls'}
]
// console.log(JSON.stringify(users))
res.end(`callback(${JSON.stringify(users)})`)
})
server.listen(3000, () => {
console.log('server is running at port 3000')
})
不单独返回JSON数据,而是会返回一段JavaScript代码,这段代码实际上是一个函数调用,传递给
callback函数作为参数的数据就是JSON数据。
前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="list">
</ul>
<script>
let list = document.getElementById('list');
// fetch("http://127.0.0.1:3000") // 跨域请求
let jsonp = (url, callback) => {
let oScript = document.createElement('script');
oScript.src = url;
document.body.appendChild(oScript);
window.callback = callback
}
jsonp("http://localhost:3000",(data) => {
list.innerHTML = data.map(item => `<li>${item.name}</li>`).join('');
window.callback = null;
})
</script>
</body>
</html>
在JSONP中,前端通过创建一个
<script>标签来请求数据,服务器响应的内容是一个JavaScript函数调用,这个函数名通常是前端指定的回调函数名。由于<script>标签加载的代码会在全局作用域中执行,因此回调函数必须是全局可见的,即挂在window对象上,这样才能确保当服务器返回的数据被执行时,浏览器能够找到并正确调用该回调函数。
默认情况下,
document.body.appendChild(oScript)会将新创建的<script>标签插入到<body>元素的末尾,即在所有已有子元素之后。
由于
script是阻塞式加载并执行的,如果不在最后插入的话,会造成callback函数未定义,在全局作用域中找不到此函数
JSONP的局限性
- 仅支持GET请求:由于依赖于
<script>标签,JSONP只能发送GET请求,无法处理POST等其他HTTP方法。 - 安全性问题:如果服务器不可信,可能会导致恶意代码被执行,存在安全隐患。
- 缺乏错误处理机制:JSONP没有内置的错误处理功能,一旦请求失败,很难捕获和处理错误。
- 全局污染:每次请求都需要挂载一个全局回调函数,容易造成全局命名空间的污染。