跨域

300 阅读4分钟

什么是跨域


  • 源 = 协议 + 域名 + 端口
  • 只要 协议、域名、端口 有一个不一样,就会引起跨域

跨域出现的场景


  • 网络通讯

    • <a/>跳转: ( 轻微限制, 几乎感觉不到有限制 )
    • 加载 css, js, 图片: ( 轻微限制, 几乎感觉不到有限制 )
    • ajax请求: ( 严厉限制 )
  • JS API

    • wind.open
    • wind.parent
    • iframe.contentWindow
  • 存储

    • webStorage
    • indexedDB

🔥window.open

场景:打开新窗口

// 当前页面:http://a.com
const newWin = window.open("http://b.com");

同源时 ✅ 可以:

  • 操作新窗口:newWin.document.write()
  • 读数据:newWin.location.href
  • 调用方法:newWin.close()

跨域时 ❌ 报错:

Uncaught DOMException: Blocked a frame with origin "a.com" from accessing a cross-origin frame.

通俗解释:

  • 你打开别人家的门可以,
  • 不能进别人家翻东西

🔥window.parent

场景:在 iframe 里访问父页面

父页面:http://a.com
iframe 里:http://b.com

iframe 内部想访问爹:

window.parent.document.write("hello");

跨域时 ❌ 直接报错

  • 浏览器不让子页面乱碰父页面

🔥iframe.contentWindow

场景:父页面想操控 iframe:

const iframe = document.querySelector("iframe");
iframe.contentWindow.document.body.style.color = "red";

跨域时 ❌ 报错

  • 父不能读子、
  • 子不能读父。

CORS


🔥最简单例子

后端返回这几个头,就实现了 CORS:

# 前端在 `http://localhost:3000`
# 请求后端 `http://localhost:8080/api`

# 浏览器一看:允许,就不拦了。

# 只能写单个域名 
# 要支持多个域名,后端必须动态判断返回
Access-Control-Allow-Origin: http://localhost:3000 
# 允许哪些请求方法
Access-Control-Allow-Methods: GET,POST,PUT,DELETE 
# 允许前端传哪些请求头
Access-Control-Allow-Headers: Content-Type, Authorization, token 
# 是否允许跨域带 Cookie
Access-Control-Allow-Credentials: true 
# 允许前端读哪些响应头
Access-Control-Expose-Headers: Content-Type, Authorization, token 
# 有效期内不再发 OPTIONS请求
Access-Control-Max-Age: 86400 

🔥带 Cookie 的跨域

  • 最容易踩坑
  • 要同时满足 3 点

1、前端

// 前端域名: a.com
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://b.com/api');
xhr.withCredentials = true; // 核心:允许跨域携带 Cookie
xhr.send(formData);

// 或者
fetch('https://b.com/api', {
  method: 'POST',
  credentials: 'include', // 核心:等价于 withCredentials: true
  body: formData
});

2、后端

# 后端域名: b.com
Access-Control-Allow-Origin: 你的前端域名(a.com) 
Access-Control-Allow-Credentials: true

3、Cookie 属性

# 1. 必须 HTTPS
SameSite=None; Secure;

🔥跨域获取响应头

问题背景

  • 默认情况:跨域 → 只能看到这几个响应头

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma
  • 其他的你通通拿不到!比如:

    • token
    • Authorization
    • sign
    • x-request-id
    • x-total-count(分页总数)
  • 解决方案

    • 因为跨域是不安全的,浏览器怕你随便拿敏感头
    • 后端必须明确告诉浏览器:哪些头可以暴露给前端 JS。
    • 你要暴露几个,就写几个,用逗号分隔。
    • 加上之后,前端 JS 才能读到。
Access-Control-Expose-Headers: token,Authorization,x-total-count

具体例子

后端要让前端拿到分页总数 x-total-count

后端返回头:

Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: x-total-count, token

x-total-count: 100
token: abc123

前端就能拿到:

// fetch
const res = await fetch(...)
const total = res.headers.get('x-total-count')
const token = res.headers.get('token')

// xhr
xhr.getResponseHeader('x-total-count')

JSONP

  • JSONP 使用简单且兼容性不错,但是只限于 get 请求
  • 不安全, 不建议使用
  • JSONP 的原理很简单,就是利用

🔥 简单例子

// 注意: 要先定义函数,再添加<script>标签
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>   
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>

🔥封装 jsonp

// 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现

function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //  @ 1 : 定义 call back 函数
        window[cb] = function (data) {
            resolve(data)
            document.body.removeChild(script)
        }

        //  @ 2 : 设置url参数  'https://www.sdf.com' ---> 'https://www.sdf.com?wd=a&cb=callback'
        url = `${url}?${stringify(params)}&cb=${cb}`

        //  @ 3 : 添加 script 标签
        let script = document.createElement("script")
        script.src = url
        document.body.appendChild(script) // script标签将会调用window[cb]函数
    })
}

function stringify(params) {
  let list = []
  for(let key in params) {
    list.push(`${key}=${params[key]}`)
  }
  return list.join('&')
}

// 使用

jsonp({
    url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',     // ------>> 这是百度搜索的接口
    params: {wd: "b"},
    cb: 'show'
})
.then(res => console.log(res)) 

代理

🔥webpack 开发环境代理

✅ 前端请求 /user/list
✅ 转发到 http://localhost:8080/user/list

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: './dist',
    port: 3000,
    open: true,

    // 🔥 代理跨域核心
    proxy: {
      '/': {
        target: 'http://localhost:8080', // 后端地址
        changeOrigin: true, // 必须开
        secure: false, // 如果是 https 但证书不安全,打开
      }
    }
  }
};

方案选择

🔥情况1

  • 背景:

    • 生产环境没有跨域问题
  • 方案:

    • 生产环境无需处理
    • 开发环境使用代理(webpack)

🔥情况2

  • 背景:

    • 生产环境有跨域问题
    • 不需要支持古老浏览器
  • 方案:

    • 生产环境使用CORS
    • 开发环境使用CORS

🔥情况3

  • 背景:

    • 生产环境有跨域问题
    • 需要支持古老浏览器
  • 方案:

    • 生产环境JSONP
    • 开发环境JSONP