必备-7.同源及跨域
同源与跨域
同源与跨域?
-
客户端给服务器端发送请求
-
同源:客户端的
http协议、域名、端口号
与服务器的三者完全相同 -
跨域:只要客户端的
http协议、域名、端口号
其中一个与服务器的这三者不同
真实项目开发中,基本都是跨域的还是同源的?
- 开发环境:我们在本地开发项目,我们需要调用后台的接口,此时前后端代码基本不在一起,这样的访问是跨域的
- 生产环境:
- 如果我们把前后端代码放在同一个服务器下,这样数据的请求是同源的【很少】
- 一般我们都是分服务器部署[web服务器、数据服务器。。。]这样还是跨域【常用】
- 我们还会在自己的项目中,调用第三方平台的接口获取数据,这样的还是跨域请求!!
为什么会产生跨域?
- 服务器资源分离:WEB服务器、数据服务器、
- 云信息共享:第三方API接口
- 有助于分离开发:开发跨域、部署同源
解决跨域方法
如何解决跨域请求?
按常用优先级:
- 第一种:PROXY跨域代理&nginx反向代理【目前最常用】
- 第二种:CORS跨域资源共享
- 第三种:JSONP
其余方案:postMessage、document.domain+iframe、window.name、location.hash…【不常用了】
-
JSONP:
- ==原理:==利用ajax/fetch存在域的限制(浏览器的安全策略),但是
script/link/img...
不存在域的限制;- 而JSONP的原理就是基于
<script src="接口请求地址">
向服务器发送请求,绕过域的限制,实现跨域数据访问。
- 而JSONP的原理就是基于
- ==缺点:==JSONP只能发送GET请求,因为script请求都是get请求,不支持POST请求
- 具体操作:
- 第一步:客户端先创建一个"全局函数" func
- 第二步:基于script的src发送请求,同时把全局函数func传递给服务器:
src="http://api.qq.com/?callback=func"
- 第三步:服务器接受请求,并且获取传递进来的函数,基本callback接收
- 第四步:准备客户端需要的数据,准备好之后,给客户端返回这样格式的内容:“func(准备好的数据)”
- 第五步:客户端获取服务器返回的信息,拿到信息之后,发现是吧第一步准备的全局函数执行,把数据作为实参传递给函数,这样我们在全局函数中就可以获取服务器准备的数据。
- ==特点:==JSONP需要服务器端的支持:服务器端存在对应的callback函数。
- ==原理:==利用ajax/fetch存在域的限制(浏览器的安全策略),但是
-
CORS:
-
==原理:==默认情况下,因为浏览器的安全策略,服务器端不允许非同源客户端请求的;如果想跨域访问,只需要服务器允许即可,所以cors跨域资源共享,就是服务器设置允许访问的源。
-
具体操作:服务器端设置
Access-Control-Allow-Origin
:-
res.header("Access-Control-Allow-Origin","访问源");//res是响应头
res.header("Access-Control-Allow-Credentials",true);//如果设置所有源访问,需要设置这个为true
res.header("Access-Control-Allow-Headers","Content-Type",...);//允许客户端发送的请求头参数 res.header("Access-Control-Allow-Methods","POST,GET,HEADR...");//允许客户端发送的方法
-
“*”:允许所有源访问,【不安全,不允许携带资源凭证】
-
“http://127.0.0.1:5500”:只允许某个源
-
但是无法直接实现允许多个源,
-
-
基于CROS给服务器发送请求,会发两次请求,第一次发一个OPTIONS方式的请求,查看是否能够请求成功,成功后再发送第二次获取数据的请求。
-
-
PROXY跨域代理&nginx反向代理:代理服务器
- 原理:服务器与服务器之间不存在跨域问题,客户端启动代理服务器(webpack/node):
帮我们预览客户端项目
:基于代理服务器可以找到同项目下的页面服务环境,作用如同open-with-liveserver。- 同时实现数据请求的代理
- 此时客户端和代理服务器是同源的
- 服务器和服务器之间不存在跨域的限制
- 过程:
- 接收客户端发送的同源请求,紧接着看到真正的服务器上获取真实的数据【服务器和服务器之间不存在跨域问题】,然后把从真实服务器获取的数据返回客户端。
- ==核心:==在于设置一个代理服务器[开发环境下我们可以基于webpack-dev-server/ndoe]设置,生产环境下我们一般是基于nginx实现代理服务器的
- 如何实现代理服务器:
- 可以基于webpack-dev-server创建【通过http-proxy】
-
- 原理:服务器与服务器之间不存在跨域问题,客户端启动代理服务器(webpack/node):
手撕JSONP
- 我们最终需要拼装出来一个
<script src="http://api.qq.com/?callback=func></script>"
这样的子串 - jsonp内部实现要点:
- 第一步:获取客户传入的url和options:
- url必须是字符串类型:否则报错:
if (typeof url !== "string") throw new TypeError('url is not a string~');
- 如果options没传:则默认为空对象
if (!isPlainObject(options)) options = {};
- url必须是字符串类型:否则报错:
- 第二步:将默认值options对象与用户传入的对象合并:
Object.assign({params:null,jsonpName: 'callback'},params)
- 第三步:返回一个Promise实例:Promise实例内部有如下操作
- 1、创建一个随机的全局函数:
window[name]=function(){}
- 全局函数是我们从服务器端获取到数据后会自动执行的函数:
func***("服务器返回的数据")
- 所以能执行到这个函数体内则表示数据获取成功,在函数体内返回正确结果就可以
resolve("服务器返回的数据")
- 也可以对性能做一些优化:删除用完的script标签,删除window[name]生成的随机私有属性name
- 全局函数是我们从服务器端获取到数据后会自动执行的函数:
- 2、拼接URL字符串:
- 我们最终需要拼装出来一个
<script src="http://api.qq.com/?callback=func></script>"
这样的子串 - 当
options
中有参数params
并且参数是个对象时,将params拼接到url后面:url += ${url.includes('?') ? '&' : '?'}${params}
; - 最后需要拼上我们请求的
jsonpName
:url += ${url.includes('?') ? '&' : '?'}${jsonpName}=${name}
;
- 我们最终需要拼装出来一个
- 3.基于script发送请求:
- 创建script标签:
script = document.createElement('script');
- 将url添加到src中:
script.src = url;
- 使script编程异步的:
script.async = true;
- 将节点放到最后:
document.body.appendChild(script);
- 如果报错就执行reject:
script.onerror = () => reject();
- 创建script标签:
- 1、创建一个随机的全局函数:
- 第四步:暴露API
- 如果是客户端,暴露给window:
if (typeof window !== 'undefined') window.jsonp = jsonp;
- 如果是服务器端,暴露给module.export:
if (typeof module === 'object' && typeof module.exports === 'object') module.exports = jsonp;
- 如果是客户端,暴露给window:
- 第一步:获取客户传入的url和options:
/*
封装一个jsonp方法「基于promise管理」,执行这个方法可以发送jsonp请求
jsonp([url],[options])
options配置项
+ params:null/对象 问号参数信息
+ jsonpName:'callback' 基于哪个字段把全局函数名传递给服务器
+ ...
*/
(function () {
//判断是不是普通标准对象
const isPlainObject = function isPlainObject(obj) {
let proto, Ctor;
if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true;
Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
//拼接字符串方法
function toencodedstring(obj){
let keys=Reflect.ownKeys(obj);
let str="";
keys.forEach(item=>{
str+=`&${item}=${obj[item]}`;
})
nstr=str.slice(1);
return nstr;
}
// jsonP方法
const jsonp=function jsonp(url,options){
// 写一个默认的options
let defaultOptions={
params:null,
jsonName:"callback"
}
// 第一步:验证传入的两个参数的格式是否符合规范
if(typeof url!=="string") throw new TypeError("url must be a string");
if(!isPlainObject(options)) options={};
// 第二步:将默认参数对象与用户传入的参数对象合并
let {params,jsonName}= Object.assign(defaultOptions,options);
// 第三步:返回一个Promise对象
return new Promise((resolve,reject)=>{
// 1、创建一个随机数,可以加时间戳或者随机数
let name=`jsonp${+new Date()}`,script;
// 2、将随机数作为window的一个私有函数
window[name]=function(da){
// 执行到这一步就说明已经从服务器拿回了数据,直接输出数据即可
resolve(da);
// 优化:
delete window[name];
if(script) document.body.removeChild(script);
}
// 拼接字符串
if(params&&isPlainObject(params)){
url+=`${url.includes('?')?"&":"?"}${toencodedstring(params)}`;
}
url+=`${url.includes('?')?"&":"?"}${jsonName}=${name}`;
// 将字符串传给script标签
script=document.createElement("script");
script.src=url;
script.onerror=()=>reject();
script.async=true;
document.body.appendChild(script);
})
}
//暴露API
if(typeof window!=='undefined') window["jsonp"]=jsonp;
if(typeof module=='object'&& typeof module.exports=='object') module.exports=jsonp;
})()
jsonp("https://www.baidu.com/sugrec",{
params:{
prod:"pc",
wd:"史佳豪"
},
jsonName:"cb"
}).then(resolved=>{
console.log(resolved);
})
跨域身份验证方案
跨域身份验证的三种方案?
-
方式一:登录态校验
- 第一步:客户端发送POST请求做登录态校验,服务器端检验用户名密码是否准确,如果正确则返回成功
- 第二步:客户端收到成功响应后,在本地手动设置cookie,存储登录成功,例如:
isLogin=true
- 第三步:之后再访问页面,检测本地是否有isLogin的cookie信息,有表示已经登陆过了,无需再验证
- 缺陷:
- 不准确,不安全,cookie可以随便修改
-
方式二:客户端设置cookie:会话存储
- 第一步:客户端发送POST请求登录,
- 第二步:服务器做账号密码验证,如果正确在响应头中设置session信息:
Set-Cookie:connect.sid
- 第三步:客户端收到响应头中有
Set-Cookie:connect.sid
,会在缓存中存储一份 - 第四步:之后再访问页面,客户端会携带
connect.sid
请求校验,服务器会查找自己的session来看是否匹配成功 - 缺陷:
- session中有信息则是登录,否则没登录
- session容易丢失【过去时间、服务器重启,session信息就没有了】
- 和cookie相关,它也容易丢失
- 不利于服务器的分布式
-
方式三:使用Token[常用]
- 第一步:客户端向服务器发送登录请求,服务器进行校验:
- 账号密码如果正确:基于JWT算法,生成一个Token信息【含有登录者、过期日期等待信息】,“服务器端不需要存储这个Token”
- 第二步:服务器手动把Token给客户端
- 第三步:客户端获取Token后,存储到本地:可以用
localStorage、sessionStroage、vuex/redux...
- 第四步:再次访问首页时,需要手动将缓存的Token发给服务器端==【基于axios的请拦截器】==
- 第五步:服务器拿到Token后,再基于JWT反反解析,验证它的有效性
- 优点:实时校验,要稳定以及有利于服务器分布
- 缺点:性能略低[忽略],需要自己写代码。
- 第一步:客户端向服务器发送登录请求,服务器进行校验:
后台开发权限管理
- 权限信息:
- 获取权限信息后,我们一般存储在数据库中,这样以后各个模块都可以获取权限信息
- 如果不存储:以后需要权限信息,重新发送请求即可
- 如果存储:不建议使用本地存储(缓存:cookie、localeStorage、sessionStroage),因为不安全,别人可能会修改权限标识[vuex/redux]
- 获取权限信息后,我们一般存储在数据库中,这样以后各个模块都可以获取权限信息
- 权限校验三类方法:
- 1、可以让用户看到,但是操作的时候要进行权限校验【无权则提示】
- 2、没有权限就渲染【为了安全保证,直接不渲染结构,而不是控制display:none】
- 3、同样的页面,根据用户权限不同,获取的信息不同,例如:进入客户列表,有的人看到的是全公司的用户,有的人只能看到自己的用户
- 结论:不管如何,只要是客户端处理权限,就不一定100%安全,所以需要服务器进行权限的二次校验【尤其是重点信息】