谈谈跨域

101 阅读6分钟

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 AURL B是否同源原因
example.comexample.com/api全部相同
example.comexample.com协议不同
example.comapi.example.com域名不同
example.com:8example.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 文件可以执行。但无法直接访问其内容(需符合 CORSJSONP 规则)。
字体/图片资源部分跨域资源(如字体、Canvas 的 toDataURL())可能受限制

实现跨域的方法

基本方向

主要分成3类:

  • 【官方主动配置】使用官方支持的CORS
  • 【绕过限制】
    • 使用【服务器】转发请求(不用浏览器了,自然不会被浏览器规则限制)
    • 利用早期浏览器遗留下来的【漏洞】【设计缺陷】;
    • 使用比较【不安全】的浏览器(现已不支持)

CORS【主动配置】【官方解决方案、正规军】

跨域资源共享,它实质上是一种基于HTTP头的安全协商机制,而非服务器主动拦截。

CORS背后的基本思路是使用自定义的HTTP头部允许浏览器和服务器相互了解。

预检请求一种服务器验证机制。对于【复杂请求】,会先发一个options方法的请求(类似于先对其一下大方向,如果大方向不同,那就不需要往后商议了)。

1.简单请求

方法:GET/POST方法

请求头:

  • 只能包含 AcceptAccept-LanguageContent-Language
  • Content-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded

我们经常使用的 applicaction/json是复杂头哦!!!!

流程:

浏览器直接发送。满足Access-Control-Origin等条件,直接通过。

2.复杂请求

非常用方法:如 PUTDELETEPATCH 等。

自定义头:如 AuthorizationX-Custom-Header

特殊 Content-Type:如 application/json

流程:

  1. 浏览器先发送options预检请求,询问服务端是否允许跨域
  2. 服务器响应 Accept-Control-Allow-*头,确认权限
  3. 浏览器发送真实请求,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,如果本地环境与测试环境联调,也需要解决“跨域”问题。