不要再问我跨域的问题了

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到教程。

跨域这两个字就像一块狗皮膏药一样黏在每一个前端开发者身上,无论你在工作上或者面试中无可避免会遇到这个问题。为了应付面试,我每次都随便背几个方案,也不知道为什么要这样干,反正面完就可以扔了,我想工作上也不会用到那么多乱七八糟的方案。到了真正工作,开发环境有webpack-dev-server搞定,上线了服务端的大佬们也会配好,配了什么我不管,反正不会跨域就是了。日子也就这么混过去了,终于有一天,我觉得不能再继续这样混下去了,我一定要彻底搞懂这个东西!于是就有了这篇文章。

要掌握跨域,首先要知道为什么会有跨域这个问题出现

确实,我们这种搬砖工人就是为了混口饭吃嘛,好好的调个接口告诉我跨域了,这种阻碍我们轻松搬砖的事情真恶心!为什么会跨域?是谁在搞事情?为了找到这个问题的始作俑者,请点击浏览器的同源策略。
这么官方的东西真难懂,没关系,至少你知道了,因为浏览器的同源策略导致了跨域,就是浏览器在搞事情。
所以,浏览器为什么要搞事情?就是不想给好日子我们过?对于这样的质问,浏览器甩锅道:“同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。”
这么官方的话术真难懂,没关系,至少你知道了,似乎这是个安全机制。
所以,究竟为什么需要这样的安全机制?这样的安全机制解决了什么问题?别急,让我们继续研究下去。

没有同源策略限制的两大危险场景

据我了解,浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。

没有同源策略限制的接口请求

有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
这就是传说中的CSRF攻击浅谈CSRF攻击方式。
看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookie、Cookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookieWeb安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

没有同源策略限制的Dom查询

1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

跨域正确的打开方式

经过对同源策略的了解,我们应该要消除对浏览器的误解,同源策略是浏览器做的一件好事,是用来防御来自邪门歪道的攻击,但总不能为了不让坏人进门而把全部人都拒之门外吧。没错,我们这种正人君子只要打开方式正确,就应该可以跨域。
下面将一个个演示正确打开方式,但在此之前,有些准备工作要做。为了本地演示跨域,我们需要:
1.随便跑起一份前端代码(以下前端是随便跑起来的vue),地址是http://localhost:9099。
2.随便跑起一份后端代码(以下后端是随便跑起来的node koa2),地址是http://localhost:9971。

同源策略限制下接口请求的正确打开方式

1.JSONP
在HTML标签里,一些标签比如script、img这样的获取资源的标签是没有跨域限制的,利用这一点,我们可以这样干:

后端写个小接口

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async jsonp (ctx) {// 前端传过来的参数const query = ctx.request.query// 设置一个cookiesctx.cookies.set('tokenId', '1')// query.cb是前后端约定的方法名字,其实就是后端返回一个直接执行的方法给前端,由于前端是用script标签发起的请求,所以返回了这个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`}
}
module.exports = CrossDomain

简单版前端

<!DOCTYPE html>
<html><head><meta charset="utf-8"></head><body><script type='text/javascript'>// 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。window.jsonpCb = function (res) {console.log(res)}</script><script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script></body>
</html>

简单封装一下前端这个套路

/*** JSONP请求工具* @param url 请求的地址* @param data 请求的参数* @returns {Promise<any>}*/
const request = ({url, data}) => {return new Promise((resolve, reject) => {// 处理传参成xx=yy&aa=bb的形式const handleData = (data) => {const keys = Object.keys(data)const keysLen = keys.lengthreturn keys.reduce((pre, cur, index) => {const value = data[cur]const flag = index !== keysLen - 1 ? '&' : ''return `${pre}${cur}=${value}${flag}`}, '')}// 动态创建script标签const script = document.createElement('script')// 接口返回的数据获取window.jsonpCb = (res) => {document.body.removeChild(script)delete window.jsonpCbresolve(res)}script.src = `${url}?${handleData(data)}&cb=jsonpCb`document.body.appendChild(script)})
}
// 使用方式
request({url: 'http://localhost:9871/api/jsonp',data: {// 传参msg: 'helloJsonp'}
}).then(res => {console.log(res)
})

2.空iframe加form
细心的朋友可能发现,JSONP只能发GET请求,因为本质上script加载资源就是GET,那么如果要发POST请求怎么办呢?

后端写个小接口

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async iframePost (ctx) {let postData = ctx.request.bodyconsole.log(postData)ctx.body = successBody({postData: postData}, 'success')}
}
module.exports = CrossDomain

前端

const requestPost = ({url, data}) => {// 首先创建一个用来发送数据的iframe.const iframe = document.createElement('iframe')iframe.name = 'iframePost'iframe.style.display = 'none'document.body.appendChild(iframe)const form = document.createElement('form')const node = document.createElement('input')// 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.iframe.addEventListener('load', function () {console.log('post success')})form.action = url// 在指定的iframe中执行formform.target = iframe.nameform.method = 'post'for (let name in data) {node.name = namenode.value = data[name].toString()form.appendChild(node.cloneNode())}// 表单元素需要添加到主文档中.form.style.display = 'none'document.body.appendChild(form)form.submit()// 表单提交后,就可以删除这个表单,不影响下次的数据发送.document.body.removeChild(form)
}
// 使用方式
requestPost({url: 'http://localhost:9871/api/iframePost',data: {msg: 'helloIframePost'}
})

3.CORS

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。看名字就知道这是处理跨域问题的标准做法。CORS有两种请求,简单请求和非简单请求。

这里引用上面链接阮一峰老师的文章说明一下简单请求和非简单请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

1.简单请求
后端

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async cors (ctx) {const query = ctx.request.query// *时cookie不会在http请求中带上ctx.set('Access-Control-Allow-Origin', '*')ctx.cookies.set('tokenId', '2')ctx.body = successBody({msg: query.msg}, 'success')}
}
module.exports = CrossDomain

前端什么也不用干,就是正常发请求就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单请求例子会看到。

fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => {console.log(res)
})

2.非简单请求
非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。

后端

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async cors (ctx) {const query = ctx.request.query// 如果需要http请求中带上cookie,需要前后端都设置credentials,且后端设置指定的originctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')ctx.set('Access-Control-Allow-Credentials', true)// 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)// 这种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headersctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')ctx.cookies.set('tokenId', '2')ctx.body = successBody({msg: query.msg}, 'success')}
}
module.exports = CrossDomain

一个接口就要写这么多代码,如果想所有接口都统一处理,有什么更优雅的方式呢?见下面的koa2-cors。

const path = require('path')
const Koa = require('koa')
const koaStatic = require('koa-static')
const bodyParser = require('koa-bodyparser')
const router = require('./router')
const cors = require('koa2-cors')
const app = new Koa()
const port = 9871
app.use(bodyParser())
// 处理静态资源 这里是前端build好之后的目录
app.use(koaStatic(path.resolve(__dirname, '../dist')
))
// 处理cors
app.use(cors({origin: function (ctx) {return 'http://localhost:9099'},credentials: true,allowMethods: ['GET', 'POST', 'DELETE'],allowHeaders: ['t', 'Content-Type']
}))
// 路由
app.use(router.routes()).use(router.allowedMethods())
// 监听端口
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)

前端

fetch(`http://localhost:9871/api/cors?msg=helloCors`, {// 需要带上cookiecredentials: 'include',// 这里添加额外的headers来触发非简单请求headers: {'t': 'extra headers'}
}).then(res => {console.log(res)
})

4.代理
想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了。
Nginx配置

server{# 监听9099端口listen 9099;# 域名是localhostserver_name localhost;#凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 location ^~ /api {proxy_pass http://localhost:9871;}    
}

前端就不用干什么事情了,除了写接口,也没后端什么事情了

// 请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
fetch('http://localhost:9099/api/iframePost', {method: 'POST',headers: {'Accept': 'application/json','Content-Type': 'application/json'},body: JSON.stringify({msg: 'helloIframePost'})
})

Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。

同源策略限制下Dom查询的正确打开方式

1.postMessage
window.postMessage() 是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。
为了演示方便,我们将hosts改一下:127.0.0.1 crossDomain.com,现在访问域名crossDomain.com就等于访问127.0.0.1。

这里是http://localhost:9099/#/crossDomain,发消息方

<template><div><button @click="postMessage">给http://crossDomain.com:9099发消息</button><iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe></div>
</template><script>
export default {mounted () {window.addEventListener('message', (e) => {// 这里一定要对来源做校验if (e.origin === 'http://crossdomain.com:9099') {// 来自http://crossdomain.com:9099的结果回复console.log(e.data)}})},methods: {// 向http://crossdomain.com:9099发消息postMessage () {const iframe = window.frames['crossDomainIframe']iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')}}
}
</script>

这里是http://crossdomain.com:9099,接收消息方

<template><div>我是http://crossdomain.com:9099</div>
</template><script>
export default {mounted () {window.addEventListener('message', (e) => {// 这里一定要对来源做校验if (e.origin === 'http://localhost:9099') {// http://localhost:9099发来的信息console.log(e.data)// e.source可以是回信的对象,其实就是http://localhost:9099窗口对象(window)的引用// e.origin可以作为targetOrigine.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);}})}
}
</script>

结果可以看到:

clipboard.png

2.document.domain
这种方式只适合主域名相同,但子域名不同的iframe跨域。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。

3.canvas操作图片的跨域问题
这个应该是一个比较冷门的跨域问题,张大神已经写过了我就不再班门弄斧了解决canvas图片getImageData,toDataURL跨域问题

最后

希望看完这篇文章之后,再有人问跨域的问题,你可以嘴角微微上扬,冷笑一声:“不要再问我跨域的问题了。”
扬长而去。

 

转自:https://segmentfault.com/a/1190000015597029

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/448378.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SSM集合

SSM集成 1. Spring和各个框架的整合 Spring目前是JavaWeb开发中最终的框架&#xff0c;提供一站式服务&#xff0c;可以其他各个框架整合集成 Spring整合方案 1.1. SSH ssh是早期的一种整合方案 Struts2 &#xff1a; Web层框架 Spring &#xff1a; 容器框架 Hibernate &#…

浅谈 CSRF 攻击方式

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一.CSRF是什么&#xff1f; CSRF&#xff08;Cross-site request forgery&#xff09;&#xff0c;中文名称&#xff1a;跨站请求伪造&a…

C++之运算符重载(上)

1、概念 所谓重载&#xff0c;就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义&#xff0c;使之实现新功能&#xff0c;因此&#xff0c;一个函数名就可以用来代表不同功能的函数&#xff0c;也就是”一名多用”。 运算符也可以重载。实际上&#xff0c;我们…

C++之运算符重载(下)

4.提高 1.运算符重载机制 编译器实现运算符重载实际上就是通过函数重载实现的&#xff0c;可分为全局函数方式&#xff0c;也可分为成员函数方式进行重载&#xff0c;并没有改变原操作符的属性和语义。只是针对某个特定类定义一种新的数据类型操作。 2.重载赋值运算符 赋值…

Cookie / Session 的机制与安全

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Cookie和Session是为了在无状态的HTTP协议之上维护会话状态&#xff0c;使得服务器可以知道当前是和哪个客户在打交道。本文来详细讨论C…

Servlet快速入门及运行流程

一、Servlet快速入门 1.创建一个web工程 2.在JavaResource中src下创建一个包名称为com.myxq.servlet 3.在创建的servlet包当中创建一个class文件起名为FirstServlet 4.进入该class实现一个Servlet接口&#xff0c;实现它未实现的方法 重点看service方法在该方法当中写入一句话进…

C++之多继承

1.基础知识 1.1 类之间的关系 has-A&#xff0c;uses-A 和 is-A has-A 包含关系&#xff0c;用以描述一个类由多个“部件类”构成。实现has-A关系用类成员表示&#xff0c;即一个类中的数据成员是另一种已经定义的类。 常和构造函数初始化列表一起使用 uses-A 一个类部分地…

聊一聊 cookie

我们看到的 cookie 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我自己创建了一个网站&#xff0c;网址为http://ppsc.sankuai.com。在这个网页中我设置了几个cookie&#xff1a;JS…

const的用法,特别是用在函数后面

原文出处&#xff1a;http://blog.csdn.net/zcf1002797280/article/details/7816977

图解 Linux 安装 JDK1.8 、配置环境变量

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 到官网下载 JDK 1.8 https://www.oracle.com/technetwork/java/javase/downloads/index.html 2. 用 rz 命令把 jdk-8u191-linux-x6…

C++之纯虚函数和抽象类

纯虚函数和抽象类 1.基本概念 2.案例 #include <iostream> using namespace std;////面向抽象类编程(面向一套预先定义好的接口编程)//解耦合 ....模块的划分class Figure //抽象类 { public://阅读一个统一的界面(接口),让子类使用,让子类必须去实现virtual void get…

解决: -bash: $‘\302\240docker‘: command not found

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我只是运行 一条很简单的启动容器的命令&#xff0c;多次执行都报错&#xff0c;报错如题&#xff1a; -bash: $\302\240docker: comma…

【汽车取证篇】GA-T 1998-2022《汽车车载电子数据提取技术规范》(附下载)

【汽车取证篇】GA-T 1998-2022《汽车车载电子数据提取技术规范》&#xff08;附下载&#xff09; GA-T 1998-2022《汽车车载电子数据提取技术规范》标准—【蘇小沐】 总结 公众号回复关键词【汽车取证】自动获取资源合集&#xff0c;如链接失效请留言&#xff0c;便于…

解决: Client does not support authentication protocol requested by server; consider upgrading MySQL

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 在服务器上把 mysql 装好后&#xff0c;运行起来。 2. navicat 死活连接不上&#xff0c;在网上查说是要改数据库账号、密码什么的&…

C++之STL理论基础

1.基本概念 STL&#xff08;Standard Template Library&#xff0c;标准模板库)是惠普实验室开发的一系列软件的统称。虽然主要出现在C中&#xff0c;但在被引入C之前该技术就已经存在了很长的一段时间。 STL的从广义上讲分为三部分&#xff1a;algorithm&#xff08;算法&am…

解决maven打包报错:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、报错经历&#xff1a; 今天使用eclipse通过maven install打war包的时候&#xff0c;出现了下图所示的错误 二、问题分析&#xff1a…

STL之Vector

1.简介 vector是将元素置于一个动态数组中加以管理的容器。可以随机存取元素&#xff08;支持索引值直接存取&#xff0c;用[]操作符或at()方法&#xff0c;还支持迭代器方式存取&#xff09;。   vector尾部添加或移除元素非常快速。但是在中部或头部插入元素或移除元素比…

解决 : Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 执行 maven install 命令报错如题&#xff1a; Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:comp…

Docker 镜像 重命名

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 镜像改名命令格式&#xff1a; # 命令格式&#xff1a;docker tag 镜像id 仓库&#xff1a;标签或&#xff1a;docker tag 旧镜…

Java蓝桥杯02——第二题集锦:生日蜡烛、星期一、方格计数、猴子分香蕉

第二题 生日蜡烛(结果填空) 某君从某年开始每年都举办一次生日party&#xff0c;并且每次都要吹熄与年龄相同根数的蜡烛。 现在算起来&#xff0c;他一共吹熄了236根蜡烛。 请问&#xff0c;他从多少岁开始过生日party的&#xff1f; 请填写他开始过生日party的年龄数。 注意&a…