前言
- 不管是 ajax 请求、canvas 的渲染等,都有可能牵涉一个问题 - 跨域
- 跨域是因为受到了浏览器同源策略的影响
- 我们也经常遇到跨域的问题,这让我们很多功能的实现上都产生了一定影响,我们下面就来看看,如何解决跨域的问题
- 完整相册 Demo - 相册
前后端交互问题 - 跨域
浏览器同源策略
- 同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源
- 同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收
- 源 :协议、域名和端口号
不受同源策略影响的资源引入
<script></script>、<img/>、<link></link>、<iframe></iframe>-src
方案
jsonp
- jsonp - JSON width Padding
- jsonp 的原理 - 通过
<script></script>不受同源策略影响的特性实现跨域
简单使用
- 后端 node - koa
router.get('/getAjax', ctx => {
// 直接返还浏览器可执行的 js 代码
ctx.body = 'var a = 123';
});
- 前端
<script src="http://127.0.0.1:1000/getAjax"></script>
<script>
console.log(a); // 123
</script>
- 这么做有一些新的问题产生
- 变量名污染
- 交互是为了通信,我把数据传给你,你把数据传回给我,这么做就只是 js 引入,没有任何意义
动态创建 script 实现请求
- 我们根据之前的 简单使用 出现的问题,对 jsonp 的
script进行修改
- 使用
params或query进行客户端->浏览器的数据传输 - 根据需求动态创建
script - 异步问题 - 请求需要时间,那么后面会有可能使得所需数据没加载进来但你却使用了导致报错
const btn = document.querySelector('button');
btn.onclick = () => {
const jsonp = document.createElement('script');
jsonp.src = `http://127.0.0.1:3000/getAjax`;
document.head.appendChild(jsonp);
// 避免异步问题,等 onload 之后再执行
jsonp.onload = () => {
console.log(a);
}
}
- onload 这种方式感觉没什么问题,但看着挺蠢的,所以我们可以利用回调再改一下,把回调的方法名传到后台,再由后端返回执行js
- 后端 node - koa
router.get('/getAjax', ctx => {
const { cb, name } = ctx.query;
ctx.body = `${cb}({
name: '${name}',
age: 20
})`;
});
- 前端
const btn = document.querySelector('button');
btn.onclick = () => {
const jsonp = document.createElement('script');
jsonp.src = `http://127.0.0.1:3000/getAjax?name=张三&cb=cbFn`;
document.head.appendChild(jsonp);
}
function cbFn(options) {
console.log(options);
}
典型例子 - 百度搜索
- 百度接口
https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=hello&cb=succFn
- wd - 搜索关键字
- cb - 回调
jsonp存在的问题
- 只能是 get 请求
- 安全性问题
CORS跨域设置
- CORS(Cross-origin resource sharing),跨域资源共享,是一份浏览器技术的规范,用来避开浏览器的同源策略
- 该设置需要后端配合
- 实际上就是后端返回数据时携带一个特殊的头信息
Access-Control-Allow-Origin
- 当我们的浏览器通过 ajax 发送了一个请求的时候,如果该请求不同源,那么浏览器会去看一下请求头信息里面是否存在一个
Access-Control-Allow-Origin字段,且当前的域是否在该字段的值内, - 如果为真,则表示该数据是可信任的,就接收响应数据
- 否则拒绝接收
- 后端 - node - koa
ctx.set('Access-Control-Allow-Origin', 'http://127.0.0.1:8080'); // 表示只有 http://127.0.0.1:8080 可以访问
ctx.set('Access-Control-Allow-Origin', '*'); // * 为通配符,表示任何域名都可以访问
CORS 头信息设置
- CORS请求时,
XMLHttpRequest对象的getResponseHeader()只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma - 如果想拿到其他字段,就必须在
Access-Control-Expose-Headers里面指定 - 我们还需要通过两个特殊头信息设置
Access-Control-Allow-Headers- 允许requset设置的头部Access-Control-Expose-Headers- 允许客户端获取的头部key
- 后端 - node - koa
ctx.set({
'Access-Control-Allow-Origin': 'http://127.0.0.1:8080',
'Access-Control-Allow-Headers': 'Authorization',
'Access-Control-Expose-Headers': 'Authorization'
});
预检请求
-
在跨站点请求的时候,即使服务器设置了
Access-Control-Allow-Origin,如非简单请求,也会请求失败,其原因是因为在正式请求前,会有一个预检请求 -
需预检的请求要求必须首先使用
OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求 -
预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响
-
简单请求 - 满足条件:
-
method
- GET
- POST
- HEAD
-
除了被用户代理自动设置的头信息(例如
Connection,User-Agent和在 Fetch 规范中定义为 禁用首部名称 的其他头信息,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:- Accept
- Accept-Language
- Content-Language
- Content-Type(需要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
-
Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
-
-
解决方案 - 后端允许预检通过
-
后端 - node - koa
if(ctx.method === 'OPTIONS') {
ctx.set('Access-Control-Request-Method', 'POST');
return ctx.body = ''
}
后端代理
-
后端 即能接电话(响应-提供服务),还能打电话(发送请求)
-
跨域问题是由浏览器的同源策略这个安全机制引起的,我们就可以通过服务器请求数据,不由浏览器直接请求数据,解决跨域问题
-
即由同源服务器作为代理,访问其他服务器:浏览器 -> 同源服务器 -> 非同源服务器 -> 同源服务器 -> 浏览器
-
node原生
let data;
const options = {
protocol: 'http:',
hostname: '127.0.0.1',
port: 3000,
path: url.replace(/^\/api/, ''),
method,
headers,
}
const req = http.request(options, res => {
res.on('data', chunk => {
data += chunk.toString();
console.log(`BODY: ${chunk}`);
})
res.on('end', () => {
res.write(data);
console.log('完成');
})
})
req.on('error', (e) => {
console.error(`problem with request: ${e.message}`);
});
req.end();
- koa-server-http-proxy
app.use(proxy('/api', {
target: 'http://127.0.0.1:3000',
pathRewrite: {
'^/api': ''
}
}));