布吕马这三个字就像一块寡廉鲜耻一样包在每三个前端开发者身上,不论你在组织工作上或者复试中不可避免会碰到那个难题。为的是应对复试,我每天都就行了背几个计划,也不晓得为何要此种干,再说面完就可以扔了,我想组织工作上也不能加进那么多支离破碎的计划。
到了真正组织工作,开发环境有webpack-dev-server搞掂,上架了服务器端的元老们也会配好,配了甚么我不管,再说不能布吕马是了。日子也就那么混过去了,终于有一天,我觉得不能再继续此种混下去了,我一定要全盘比如说那个小东西!于是就有了这首诗。
一、背景介绍
要掌握布吕马,首先要晓得为何会有布吕马那个难题出现。
确实,他们此种搬砖工人是为的是稚鳕嘛,回去的调个USB告诉我跨域了,此种妨碍他们随心所欲搬砖的事真头痛!为何会布吕马?是谁在搞事。
那么非官方的小东西真晦涩,说实话,最少你晓得了,因为应用程序的相混思路导致了布吕马,是应用程序在搞事。
因此,应用程序为何要搞事?是不想给萨德基他们过?对于此种的反问,应用程序甩锅道:“相混思路管制了从同三个源读取的文档格式或JAVA如何与来自另三个源的资源展开可视化。这是三个用于隔绝潜在性蓄意文档的重要安全可靠监督机制。”
那么非官方的话术真晦涩,说实话,至少你晓得了,似乎这是个安全可靠监督机制。
因此,究竟为何需要此种的安全可靠监督机制?此种的安全可靠监督机制解决了甚么难题?喽,让他们继续研究下去。
二、两大脆弱情景
据我了解,应用程序是从三个方面去做那个相混思路的,一是特别针对USB的允诺,二是特别针对Dom的查阅。换言之一下没此种的管制上述两种姿势有甚么脆弱。
1.没相混思路管制的USB允诺
有三个小小小东西叫cookie大家应该晓得,一般用以处理登入等情景,目的是让服务器端晓得谁发出的这次允诺。
如果你允诺了USB展开登入,服务器端校正通过前会在积极响应头加入Set-Cookie表头,然后到时候Kozhikode允诺的时候,应用程序会手动将cookie附加在HTTP允诺的头表头Cookie中,服务器端就能晓得这个用户已经登入过了。
晓得那个之后,他们来看情景:
1.你准备去清空你的购物车,于是打开了买买买网站http://www.maimaimai.com,然后登入成功,一看,购物车小东西那么少,不行,还得买多点。
2.你在看有甚么小东西买的过程中,你的好基友发给你三个链接http://www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着http://www.nidongde.com,谁知那个网站暗地里做了些不可描述的事!由于没相混思路的管制,它向http://www.maimaimai.com发起了允诺!聪明的你一定想到上面的话“服务器端校正通过前会在积极响应头加入Set-Cookie表头,然后到时候Kozhikode允诺的时候,应用程序会手动将cookie附加在HTTP允诺的头表头Cookie中”,此种一来,那个不法网站就相当于登入了你的账号,可以为所欲为的是!如果这不是三个买买买账号,而是你的银行账号,那……
这是传说中的CSRF攻击。
看了这波CSRF攻击我在想,即使有了相混思路管制,但cookie是明文的,还不是一样能拿下来。
于是我看了一些cookie相关的文
2. 没相混思路管制的Dom查阅
1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进http://www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登入界面,你果断输入你的账号密码,登入进去看看钱有没少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是http://www.yinhang.com,而现在访问的是http://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.相混思路管制下USB允诺的正确打开方式
后端写个小USB
// 处理成功失败返回格式的工具
const {successBody} = require(../utli)
class CrossDomain {
static async jsonp (ctx) {
// 前端传过来的参数
const query = ctx.request.query
// 设置三个cookies
ctx.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.length
return 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)
delete window.jsonpCb
resolve(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允诺怎么办呢?
后端写个小USB:
// 处理成功失败返回格式的工具
const {successBody} = require(../utli)
class CrossDomain {
static async iframePost (ctx) {
let postData = ctx.request.body
console.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中执行form
form.target = iframe.name
form.method = post
for (let name in data) {
node.name = name
node.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允诺分成两类:简单允诺(simple request)和非简单允诺(not-so-simple request)。
只要同时满足以下两大条件,就属于简单允诺。
(1) 允诺方法是以下三种方法之一:HEADGETPOST(2)HTTP的头信息不超出以下几种表头:
AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain1.简单允诺
后端:
// 处理成功失败返回格式的工具
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,且后端设置指定的origin
ctx.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-Headers
ctx.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
三个USB就要写那么多代码,如果想所有USB都统一处理,有甚么更优雅的方式呢?见下面的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`, {
// 需要带上cookie
credentials: include,
// 这里添加额外的headers来触发非简单允诺
headers: {
t: extra headers
}
}).then(res => {
console.log(res)
})
4.代理
想一下,如果他们允诺的时候还是用前端的域名,然后有个小东西帮他们把那个允诺转发到真正的后端域名上,不就避免布吕马了吗?这时候,Nginx出场了。
Nginx配置:
server{
# 监听9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api那个样子的,都转发到真正的服务器端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
前端就不用干甚么事了,除了写USB,也没后端甚么事了:
// 允诺的时候直接用回前端这边的域名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,如果兼容性没难题(IE 10或者以上),CROS才是更通用的做法吧。
1
相混思路管制下Dom查阅的正确打开方式
1.postMessage
window.postMessage() 是HTML5的三个USB,专注实现不同窗口不同页面的布吕马通讯。
为的是演示方便,他们将hosts改一下:127.0.0.1 http://crossDomain.com,现在访问域名http://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(me
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 () {
//localhost:9099) {
// http://localhost:9099发来的信息
console.log(e.data)
// e.source可以是回信的对象,其实是http://localhost:9099窗口对象(window)的引用
// e.origin可以作为targetOrigin
e.source.postMessage(`我是[http://crossdomain.com:9099],我晓得了兄弟,这是你想晓得的结果:${document.getElementById(app) ? 有id为app的Dom : 没id为app的Dom}`, e.origin);
}
})
}
}
</script>
结果可以看到:
2.document.domain
此种方式只适合主域名相同,但子域名不同的iframe布吕马。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,此种情况下给三个页面指定一下document.domain即document.domain = http://crossdomain.com就可以访问各自的window对象了。
3.canvas操作图片的布吕马难题
那个应该是三个比较冷门的布吕马难题,张大神已经写过了我就不再班门弄斧了。
一、最后
希望看完这首诗之后,再有人问布吕马的难题,你可以嘴角微微上扬,冷笑一声:“不要再问我布吕马的难题了。” 扬长而去。
如果学到了可以点在看让更多的小伙伴看到哦 。
原文作者:写bug
原文链接:
https://mp.weixin.qq.com/s/RRy-XYSEADd-yELj8oMDpw