2021年一次前后端联调,后端小哥发现代码无论如何修改,始终没把cookie写进页面。
事后发现是前端的锅,没有设置跨域允许携带cookie。
那个前端就是我哈哈。
现在回想起来,还挺怀念那位乐观幽默的小哥。
浏览器跨域、cookie跨站默认不携带,都是浏览器保障安全的设计。
时序图解释跨域
sequenceDiagram
participant Browser as 浏览器
participant Page as 当前页面<br>(https://domain-a.com)
participant Server as 目标服务器<br>(https://domain-b.com/api)
Note over Browser: 同源策略限制:协议/域名/端口需一致
Page->>Browser: 发起跨域请求 (fetch/ajax)
Browser->>Server: 发送请求 (自动携带Origin头)
alt 服务器支持CORS
Server-->>Browser: 响应头包含:<br>Access-Control-Allow-Origin: *
Browser->>Page: 允许返回数据
else 服务器未配置CORS
Server-->>Browser: 正常响应(无CORS头)
Browser->>Page: 拦截响应(控制台报错)
end
opt 预检请求(复杂请求时)
Browser->>Server: OPTIONS 请求<br>检查CORS策略
Server-->>Browser: 返回允许的方法/头
Browser->>Server: 发送正式请求
end
什么情况算跨域
同源策略
同源策略是浏览器最核心的安全机制之一。
不同源即跨域: 【协议】 、 【域名】、【端口】 有任何一个不相同,则算两个网页跨域。
https :// bank.com:443/user/getMsg
- 如上 https 为协议,通常还有 http
- bank.com 为域名
- 443为端口号,对于http默认是80,https默认443(但这是可以指定的,你也可以就指定 8088)
两个 URL 的 协议(Protocol)、域名(Host)、端口(Port) 必须 完全相同 才属于同源。
| URL A | URL B | 是否同源 | 原因 |
|---|---|---|---|
| example.com | example.com/api | ✅ | 全部相同 |
| example.com | example.com | ❌ | 协议不同 |
| example.com | api.example.com | ❌ | 域名不同 |
| example.com:8 | example.com:443 | ❌ | 端口不同 |
跨域与跨站
跨域更严格。协议、域名、端口 任一不同(同源策略的严格限制)xx.a.com
跨站只要 顶级域名(eTLD+1) 相同,就不算跨站。
eTLD(有效顶级域名)
对于 www.example.com,eTLD 指的是 .com,eTLD + 1 指的是example.com
shop.example.com → blog.example.com (共享 example.com)【同站】但是【跨域】
https://a.com → http://a.com(协议不同,但是也算【同站】)【跨域】
- 跨域:像两家完全独立的餐厅(
KFC和麦当劳)
-
- 你不能用 KFC 的会员卡在麦当劳打折(数据隔离)。
- 除非两家签了合作协议(CORS)。
- 跨站:像同一品牌的不同分店(
KFC 北京店和KFC 上海店)
-
- 会员卡默认通用(Cookie 同站共享)。
- 但某些活动可能仅限本地(SameSite=Strict)。
跨域为什么安全
同源策略下限制了哪些操作?
| 操作类型 | 概述 |
|---|---|
| xhr/fetch | 禁止通过 api 访问跨域资源。但是【请求】实际上是成功发送的。只是响应被浏览器阻断了。 |
| dom | 禁止通过 iframe``window.open``parent.document等方式跨域访问 DOM |
| 本地存储 | 禁止读取本地缓存,包括不限于 cookie/localStorage/sessionStorage |
| script标签 | 通过 <script> 加载的跨域 JS 文件可以执行。但无法直接访问其内容(需符合 CORS 或 JSONP 规则)。 |
| 字体/图片资源 | 部分跨域资源(如字体、Canvas 的 toDataURL())可能受限制 |
实现跨域的方法
基本方向
主要分成3类:
- 【官方主动配置】使用官方支持的
CORS - 【绕过限制】
-
- 使用【服务器】转发请求(不用浏览器了,自然不会被浏览器规则限制)
- 利用早期浏览器遗留下来的【漏洞】【设计缺陷】;
- 使用比较【不安全】的浏览器(现已不支持)
CORS【主动配置】【官方解决方案、正规军】
跨域资源共享,它实质上是一种基于HTTP头的安全协商机制,而非服务器主动拦截。
CORS背后的基本思路是使用自定义的HTTP头部允许浏览器和服务器相互了解。
预检请求一种服务器验证机制。对于【复杂请求】,会先发一个options方法的请求(类似于先对其一下大方向,如果大方向不同,那就不需要往后商议了)。
1.简单请求
方法:GET/POST方法
请求头:
- 只能包含
Accept、Accept-Language、Content-Language - Content-Type(仅限
text/plain、multipart/form-data、application/x-www-form-urlencoded)
【我们经常使用的 applicaction/json是复杂头哦!!!! 】
流程:
浏览器直接发送。满足Access-Control-Origin等条件,直接通过。
2.复杂请求
非常用方法:如 PUT、DELETE、PATCH 等。
自定义头:如 Authorization、X-Custom-Header。
特殊 Content-Type:如 application/json。
流程:
- 浏览器先发送
options预检请求,询问服务端是否允许跨域 - 服务器响应
Accept-Control-Allow-*头,确认权限 - 浏览器发送真实请求,
GET``POST
关键点:Content-Type: application/json 或 Authorization 头一定会触发复杂请求!
JSONP【利用特性绕过限制】
json padding,比如 callbackFn({ "name": 123}),看起来和 json 一样。
局限性只支持 GET请求。
【实例】
- 前端页面
<body>
I'm Page.
<script>
function handleResponse(data) {
console.log('🍀🍀🍀🍀', data)
}
</script>
</body>
<script src="http://content.com:8088/jsonp?callback=handleResponse"></script>
- 跨域服务端
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
if (ctx.path === '/jsonp') {
const callback = ctx.query.callback
const data = {
name: 'chp',
age: 18,
}
ctx.body = `${callback}(${JSON.stringify(data)})`
}
})
app.listen(8088, () => {
console.log('服务器启动成功,端口号为:8088')
})
【一个 jsonp 封装】
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let img = new Image()
window[callback] = function (data) {
document.body.removeChild(img)
}
img.onload = img.onerror = function(event) {
console.log('请求返回了')
}
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
img.src = `${url}?${arrs.join('&')}`
document.body.appendChild(img)
})
}
jsonp({
url: 'http://localhost:8002/say',
params: { data: '前端传点什么呢' },
callback: 'show'
}).then(data => {
console.log(data)
})
代理服务器【让服务器转发请求】
【实例】
- 前端请求,本地服务器:
content.com:3000
<script>
fetch('/api/getData')
.then((res) => res.json())
.then((res) => {
console.log('🍀🍀🍀🍀', res)
})
</script>
- 本地服务器转发三方服务地址
bank.com:8088
const Koa = require('koa')
const serve = require('koa-static')
// 引入代理中间件
const proxy = require('koa-server-http-proxy')
const app = new Koa()
app.use(serve('./'))
app.use(
proxy('/api', {
target: 'http://bank.com:8088',
pathRewrite: {
'^/api': '',
},
changeOrigin: true,
}),
)
app.listen(3000, () => {
console.log('服务器启动成功,端口号为:3000')
})
- 三方服务
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
if (ctx.path === '/getData') {
ctx.body = {
name: 'chp',
age: 23,
address: '北京'
}
}
})
app.listen(8088, () => {
console.log('服务器启动成功,端口号为:8088')
})
【请求成功】
早期的浏览器漏洞,设置允许跨域
随着 chrome 的更新,已经不允许了。
C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=C:\MyChromeDevUserData
Websocket(利用协议特性——不受“同源策略”影响的协议)
- 启动一个websocket服务器
- 启动一个静态服务器
http://127.0.0.1:3030 -> WebSocket -> ws://127.0.0.1:8888
访问成功即可说明其支持跨域。
针对 DOM 的跨域,iframe
主要是使用 postMessage,通过我们需要配置白名单 origin
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym',
type:'wuhan'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://10.73.154.73:8088');
};
// 接受domain2返回数据,这边给延迟的原因,因为同步传输时,页面不一定立马拿到数据,所以给延迟
setTimeout(function(){
window.addEventListener('message', function(e) {
alert('data from domain2 sss ---> ' + e.data);
}, false);
},10)
</script>
简单地来说,就是一边使用 postMessage 来发送。
一边通过监听 addEventListener - message 来接收。
通常会使用 origin 区分来源。
为什么我们经常遇见跨域
作为前端,很多时候,我们与后端联调的时候,是需要跨域的。
通常前端的项目,跑在我们电脑本机的ip上,如:172.17.191.1:8080
而后端同学他们的后端代码,通常也是跑在本机,172.17.191.33:8080
IP 必然不同,所以需要做跨域处理。
哪怕是API部署了,跑在测试环境(测试服务器),如:test.hh.com,如果本地环境与测试环境联调,也需要解决“跨域”问题。