Appearance
同源
同源策略是一个重要的安全策略,它用于限制一个文档或者它加载的脚本是否能与另一个源的资源 进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
如果两个 URL 的协议、域名、端口号都相同的话,则这两个 URL 是同源,只要有一个不一样, 就是跨域。ajax 请求时,为了安全,浏览器要求当客户端与服务器必须同源,否则会抛出跨域的 错误,而加载 img、link、script、iframe 等不受同源策略限制。
IE 中的特例:两个相互之间高度互信的域名,如公司域名,则不受同源策略限制。另外 IE 未将 端口号纳入到同源策略的检查中。
sh
http://store.company.com/dir/page.html
# 同源 只有路径不同
http://store.company.com/dir2/other.html
# 同源 只有路径不同
http://store.company.com/dir/inner/another.html
# 失败 协议不同
https://store.company.com/secure.html
# 失败 端口不同 http 默认端口是 80
http://store.company.com:81/dir/etc.html
# 失败 三级域名不同
http://company.com/dir2/other.html
# 失败 三级域名不同
http://news.company.com/dir/other.html
# 失败
http://192.168.1.1/dir/other.htmlCORS(跨域资源共享)
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。
js
const whiteSet = new Set(['http://127.0.0.1:56254'])
const cors = app => {
app.use(async (ctx, next) => {
const { req, res } = ctx
const { origin } = req.headers
if (!whiteSet.has(origin)) {
ctx.status = 403
ctx.body = { error: 'Origin not allowed' }
return
}
// 该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 *,表示接受任意
// 域名的请求,这个字段只能是 * 或者一个具体的值,无法通过 , 分隔设置多个值,
// 注意 http://localhost 和 http://127.0.0.1 不是同一个源,
// 这个响应头必须同时在预检请求和正式请求中设置,如果正式请求中没有设置,
// 即使预检请求通过,依然会报跨域错误
res.setHeader('Access-Control-Allow-Origin', origin)
// 该字段可选。它的值是一个字符串,表示是否允许发送 Cookie。默认情况下,
// Cookie 不包括在CORS 请求之中。设为 'true',即表示允许发送 Cookie
// 这个值也只能设为 'true',如果服务器禁止浏览器发送 Cookie,删除该字段即可
// 只有当 Access-Control-Allow-Origin 不是 "*" 时,才能返回该字段(即允许
// credentials 时必须指定具体域)。
// 允许后,前端还必须设置 withCredentials: true 才能携带 cookie。
// 这个响应头必须同时在预检请求和正式请求中设置,如果正式请求中没有设置,
// 即使预检请求通过,依然会报跨域错误
res.setHeader('Access-Control-Allow-Credentials', 'true')
if (req.method === 'OPTIONS') {
// 该字段可选,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。
// 注意,响应头中返回的是所有支持的方法,而不单是浏览器请求的那个方法,
// 这是为了避免多次"预检"请求。对于 GET, POST, HEAD, OPTIONS 方法是无法拦截的,
// 即使不写上,预检请求通过后,正式请求使用 GET, POST, HEAD 中的任意一个
// 都不会被拦截
// 只需要在预检请求中设置
res.setHeader('Access-Control-Allow-Methods', 'PUT, DELETE')
// 该字段可选,它也是一个逗号分隔的字符串,表明服务器支持的所有特殊请求头信息字段,
// 只需要在预检请求中设置
res.setHeader("Access-Control-Allow-Headers", 'Authorization, Content-Type, X-Custom-Header')
// 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天
// (1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出
// 另一条预检请求。
// 值为 -1, 表示禁用缓存,每次请求前都要发送 OPTIONS 预检请求。
// 注意 Access-Control-Max-Age 设置针对完全一样的 url,当 url 包含参数时,
// 其中一个 url 的 Access-Control-Max-Age 设置对另一个 url 没有效果。
// 只需要在预检请求中设置
res.setHeader('Access-Control-Max-Age', '3600')
return ctx.status = 204
}
// 该字段可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法
// 只能拿到 6 个基本字段:Expires、Cache-Control、Last-Modified、
// Content-Type、Content-Language、Pragma(Pragma: public,作用是告诉浏览器,
// 响应内容可以被公开缓存)。这里没有 Etag,但是不妨碍浏览器获取,因为只是限制
// xhr.getResponseHeader() 能拿到的响应头,如果想拿到其他字段,就必须在这里面
// 指定
// 只需要在正式请求中设置
res.setHeader('Access-Control-Expose-Headers', 'Foo, Bar')
// res.setHeader('Foo', '123')
await next()
})
}
module.exports = cors简单请求
如果同时满足下面两个条件,就是简单请求。
请求方法是以下三种方法之一:
HEAD
GET
POST
HTTP的头信息不超出以下几种字段
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于以下三个值
application/x-www-form-urlencoded
multipart/form-data
text/plain
一个携带 Cookie 的简单请求依然是简单请求
简单请求的基本流程
对于简单请求,浏览器认为足够安全,直接发出 CORS 请求,请求头中新增一个 Origin 字段用来说明本次请求来自哪个源(协议 + 域名 + 端口号)
sh
origin http://127.0.0.1:56254服务器会返回一个正常的 HTTP 响应,浏览器发现响应头中没有 Access-Control-Allow-Origin 字段或者有该字段,但是值和 Origin 不一致,浏览器会触发正式请求的 XMLHttpRequest 的 onerror 回调。注意,预检请求不会触发 xhr 回调,甚至预检请求不通过时,你可以给预检请求返回 200,但是正式请求依旧会报错
非简单请求的基本流程
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为预检请求(不会携带 Cookie),相比简单请求会多出 Access-Control-Request-Method 字段
sh
OPTIONS / HTTP/1.1
# 如果有设置特殊请求头,则会请求头中多出这个字段
Access-Control-Request-Headers: my-header
Access-Control-Request-Method: GET
Origin: http://127.0.0.1:56254浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及是否使用了允许的 HTTP 请求方法和请求头字段,是否允许 cookie,只有得到肯定答复,预检请求才会通过,预见请求通过后浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错,预见请求响应头中没有任何字段,被XMLHttpRequest 对象的 onerror 回调函数捕获。控制台会打印出如下的报错信息。
withCredentials
javascript
const xhr = new XMLHttpRequest()
xhr.withCredentials = true如果省略 withCredentials 设置,有的浏览器还是会一起发送 Cookie。可以显式关闭 withCredentials。
javascript
xhr.withCredentials = false正向代理
原理:利用 http-proxy-middleware 这个中间件进行正向代理,将请求转发给其他的服务器。 webpack-dev-server 会自动开启一个代理服务器,当本地发送请求的时候,代理服务器会接受 这个情求,并将这个请求转发给目标服务器,目标服务器返回数据后,代理服务器又会将数据返回 给浏览器,由于代理服务器和客户端是同源的,不会存在跨域的问题。
javascript
devServer: {
// 服务器代理,解决开发环境下跨域问题。
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api': ''},
ws: true, // 用于支持 websocket
changeOrigin: true // 控制请求头中的 host 值
},
'/api2': {
target: 'http://localhost:3001',
pathRewrite: {'^/api2': ''},
}
}
}反向代理
如 Nginx 反向代理,前端请求都是通过 Nginx 转发到 Node 服务器(如 localhost:8080),对于浏览器来说前端和后端是同源的,而服务器之间不存在跨域,因此就不需要在 Node 服务器上单独设置 CORS 了。
JSONP
原理: 基于 script 不受同源策略限制
客户端
html
<script>
// 回调函数要在 jsonp 请求之前定义好,且不能定义在 jsonp 的那个 script 中,
// 因为 script 如果设置了 src 那么里面的代码是不会被解析的。
function cb (data) {
console.log(data)
}
</script>
<script src='http://127.0.0.1:3000/getData?callback=cb'></script>服务器
js
// 服务器:
// 1.准备数据 data = {...}
// 2.给客户端返回数据 'func(' + JSON.stringify(data) + ')' 浏览器拿到这段字符串后,
// 会当作js代码解析执行
app.get('/getData', (req, res, next) => {
const { callback } = req.query
const data = {
foo: 'foo',
bar: 'bar'
}
res.send(`${ callback }(${ JSON.stringify(data) })`)
})优点
简单,兼容性好。
缺点
只能处理 GET 请求
每个请求在后台都要做处理,很麻烦。
不安全,可能遭受 xss 攻击。
并发的 jsonp 如何区分数据返回后的回调函数
每个请求都使用不同的回调,但是会带来服务器端的性能问题,因为服务器端可以通过 cdn 对相同请求进行缓存,如果回调不同会导致缓存失效,流向直接冲向源站。
如果使用相同的回调, 发送请求前判断是否有相同的 jsonp 请求正在发送,如果发生冲突 则取消本次请求,等上一个请求发送完再重新请求,也就是把并行转为串行执行。
jsonp 怎么处理多个组件需要调用的方法名相同,产生了命名冲突
js
function jsonp (url, params, callback) {
return new Promise((resolve, reject) => {
// url拼接处理
let keys = Object.keys(params),
i = 0,
length = keys.length,
key,
val,
parmasStr = '?'
for (; i < length; i++) {
key = keys[i]
val = params[key]
parmasStr += `${key}=${val}&`
}
parmasStr += `callback=${callback}`
url += parmasStr
// 创建script标签
let script = document.createElement('script')
script.src = url
// 添加回调函数, 服务器返回数据时会被执行
window[callback] = function (data) {
resolve(data)
// 移除标签
document.head.removeChild(script)
}
// 添加标签
document.head.appendChild(script)
})
}
jsonp('http://localhost:3000/getData', {name: 'Tom'}, 'getData')
.then(val => {
console.log(val)
})postMessage + iframe
html
// 3000/index.html
<iframe id='iframe' src='http://127.0.0.1:3001' frameborder='0' style="display: none;"></iframe>
<script>
// 向3001/index.html发送信息
iframe.onload = function () {
iframe.contentWindow.postMessage('message', 'http://127.0.0.1:3001')
}
// 接收信息
window.onmessage = function (ev) {
console.log(ev.data)
}
</script>
// 3001/index.html
<script>
// 接收信息
window.onmessage = function (ev) {
// ev.source代表3000/index.html
// 接收到后给3000发送信息
ev.source.postMessage(ev.data + '@@@', ev.origin)
}
</script>WebSocket
客户端
js
const socket = new WebSocket('ws://127.0.0.1:3000')
socket.addEventListener('open', () => {
console.log('connected')
})
socket.addEventListener('message', e => {
console.log(e.data)
})
sendMsg.addEventListener('click', () => {
socket.send('')
})服务器
js
const { WebSocketServer } = require('ws')
const server = new WebSocketServer({ port: 3000 })
server.on('connection', socket => {
socket.on('message', message => {
socket.send('foo')
})
})document.domain + iframe 只能实现同一个主域.不同子域之间的操作
html
// 父页面A http://www.demo.cn/A.html
<iframe src="http://school.demo.cn/B.html" frameborder="0"></iframe>
<script>
document.domain = 'demo.cn'
var user = 'admin'
</script>
// 子页面B
<script>
document.domain = 'demo.cn'
alert(window.parent.user) // window.parent就是父页面A
</script>window.name + iframe
html
// 3000
<iframe id="iframe" src="http://127.0.0.1:3001" frameborder="0" style="display: none;"></iframe>
<script>
// onload触发两次,第二次才得到信息
let count = 0
iframe.onload = function () {
if (count === 0) {
// 需要先把地址重新指向到同源中(window.name要求指向同源)
// proxy.html中只要不写window.name拿到的就是3001的window.name
iframe.src = 'http://127.0.0.1:3000/proxy.html'
count++
return
}
console.log(iframe.contentWindow.name)
}
</script>
// 3001
<script>
// 3001需要返回给3000的信息都在window.name中存储着
window.name = 'xxx'
</script>localtion.hash + iframe
A和C同源 A和B不同源
html
// A.html
<iframe id="iframe" src="http://127.0.0.1:3001/B.html" style="display: none;" frameborder="0"></iframe>
<script>
// 向B.html传hash值
let count = 0
iframe.onload = function () {
if (count === 0) {
iframe.src = 'http://127.0.0.1:3001/B.html#msg=message'
count++
return
}
}
// 开放给同源C.html的回调方法
function func (res) {
console.log(res)
}
</script>
// B.html
<iframe id="iframe" src="http://127.0.0.1:3000/C.html" style="display: none;" frameborder="0"></iframe>
<script>
// 监听A传过来的hash改变,再传给C
window.onhashchange = function () {
iframe.src = 'http://127.0.0.1:3000/C.html' + location.hash
}
</script>
// C.html
<script>
// 监听B传过来的hash值
window.onhashchange = function () {
// 再通过操作同源A的js回调,将结果传回
window.parent.parent.func(location.hash)
}
</script>