# 同源策略

同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

如果两个url的协议、域名、端口号都相同,则说明是同源的,有一个不同都是不同源的。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同
http://news.company.com/dir/other.html 失败 主机不同

同源策略下有诸多限制,以下操作在不同源的情况是不能相互操作的:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,服务端返回的结果被浏览器拦截了

但是以下三种标签是允许跨域加载资源的:

  • img
  • script
  • link

TIP

即便跨域,ajax请求也是可以发出去的,服务端也能正常响应,只是返回结果被浏览器拦截了。

跨域问题实际是由浏览器的同源策略导致的,只要两个 url 的协议、域名、端口号有一个不同就是跨域了。

# JSONP

# JSONP 初识

JSONP 主要是利用 script 标签没有跨域限制这个特性来实现的。

JSONP 由两部分组成:回调和数据。

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    function callback(data) {
      console.log(data)
    }
  </script>
  <script src="http://localhost:3002/list?callback=callback"></script>
</body>
</html>

后端代码:

const express = require('express'),
      app = express()

app.listen(3002, () => {
  console.log('ok')
})

app.get('/list', (req, res) => {
  const { callback } = req.query
  const data = {
    code: 0,
    msg: 'ok',
    data: {
      name: 'f'
    }
  }

  res.send(`${callback}(${JSON.stringify(data)})`)
})

可以看到,浏览器控制台成功打印出了接口返回的数据。

# 封装 JSONP

虽然我们上面实现了用 JSONP 请求跨域的接口数据,但是有两个不足之处:

  1. 我们在平时调用接口的时候会传递很多很多参数,如果手动将每个参数拼接到 url 中,未免有些麻烦。
  2. 上面这种写完是同步的,接口没有返回数据的话,后面的代码就无法执行。

下面我们来封装一个 jsonp 函数,来解决上述问题。

前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    function jsonp({url, params, callback='callback'}) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        window[callback] = function(data) {
          resolve(data)
          document.body.removeChild(script)
          window[callback] = null
        }
        params = { ...params, callback }
        const arr = []
        for (let key in params) {
          arr.push(`${key}=${params[key]}`)
        }
        script.src = `${url}?${arr.join('&')}`
        document.body.appendChild(script)
      })
    }

    jsonp({
      url: 'http://localhost:3002/list',
      params: {name: 'f', age: 18}
    }).then(data => {
      console.log(data)
    })
  </script>
</body>
</html>

后端代码:

const express = require('express'),
      app = express()

app.listen(3002, () => {
  console.log('ok')
})

app.get('/list', (req, res) => {
  const { callback } = req.query
  const data = {
    code: 0,
    msg: 'ok',
    data: {
      name: 'f'
    }
  }

  res.send(`${callback}(${JSON.stringify(data)})`)
})

运行代码,依然能够拿到我们想要的结果。

# JQ中的JSONP

客户端代码:

// index.js
$.ajax({
  url: "http://localhost:3002/list",
  dataType: "jsonp",
  type: "get", // 可以省略
  success: function (data) {
      console.log(data);
  }
});
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script src="./index.js"></script>
</body>
</html>

后端代码:

// server.js
const express = require('express'),
      app = express()

app.listen(3002, () => {
  console.log('ok')
})

app.get('/list', (req, res) => {
  const { callback } = req.query
  const data = {
    code: 0,
    msg: 'ok',
    data: {
      name: 'f'
    }
  }

  res.send(`${callback}(${JSON.stringify(data)})`)
})

你有没有好奇服务端中的 callback 怎么来的?来,我们一起来看一下浏览器的控制面板:

原来 callback 是JQ帮我们加上的。我们可以通过 jsonpCallbackjsonp 两个属性对其进行配置,改成我们想要的名称。





 
 





$.ajax({
  url: "http://localhost:3002/list",
  dataType: "jsonp",
  type: "get",//可以省略
  jsonpCallback: "show", // 为 jsonp 请求指定一个回调函数名
  jsonp: "callback", // 在一个 jsonp 请求中重写回调函数的名字
  success: function (data) {
      console.log(data);
  }
});

可以看到请求参数变成了我们自己设置的名字,不过一般情况下我们不用去修改这两个名称。

# 优、缺点

优点:兼容性好

缺点:

  • 服务器方必须支持。
  • 只支持 get 方式。
  • 要保证请求的域绝对安全,否则会有安全隐患。
  • 不好确定 JSONP 请求是否失败。

# CORS跨域资源共享

客户端代码

$.ajax({
  url: "http://localhost:3002/list",
  success: function (data) {
    console.log(data);
  }
});

服务端代码

const express = require('express'),
	app = express(),
	whitList = [ 'http://127.0.0.1:5501' ]; //设置白名单

app.use((req, res, next) => {
	let origin = req.headers.origin;

	if (whitList.includes(origin)) {
		// 设置哪个源可以访问我
		res.setHeader('Access-Control-Allow-Origin', origin);
		// 允许携带哪个头访问我
		res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With');
		// 允许哪个方法访问我
		res.setHeader('Access-Control-Allow-Methods', 'PUT,GET,POST,DELETE,HEAD,OPTIONS');
		// 允许携带cookie
		res.setHeader('Access-Control-Allow-Credentials', true);

		if (req.method === 'OPTIONS') {
			res.end(); // OPTIONS请求不做任何处理
		}
	}

	next();
});

app.listen(3002, () => {
	console.log('ok');
});

app.get('/list', (req, res) => {
	const { callback } = req.query;
	const data = {
		code: 0,
		msg: 'ok',
		data: {
			name: 'f'
		}
	};

	res.send(data);
});

缺点:

Access-Control-Allow-Origin 只能设置一个具体的源,但实际开发中,可能有多个源请求这个接口,如果将 Access-Control-Allow-Origin 设置为 * ,就不能携带cookie了。

# 基于http proxy实现跨域请求

安装依赖

cnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D 

webpack 配置文件

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.min.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    port: 3003,
    // 显示打包进度
    progress: true,
    contentBase: './dist',
    proxy: {
      '/': {
        target: 'http://localhost:3002',
        changeOrigin: true
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html'
    })
  ]
}

前端代码:

// src/index.js
$.ajax({
  url: "/list",
  success: function (data) {
    console.log(data);
  }
});
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</body>
</html>

后端代码:

const express = require('express'),
      app = express()

app.listen(3002, () => {
  console.log('ok')
})

app.get('/list', (req, res) => {
  const { callback } = req.query
  const data = {
    code: 0,
    msg: 'ok',
    data: {
      name: 'f'
    }
  }

  res.send(data)
})

我使用的依赖版本如下:

"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"

在终端执行 npx webpack-dev-serverCannot find module 'webpack-cli/bin/config-yargs,原因是 webpack-dev-server 命令改成了 webpack serve,需要在终端执行 npx webapck serve

# proxy解决跨域原理浅析

proxy只是一层代理,用于把指定的 path 代理去后端提供的地址,背后使用 node 来做 server。只适用本地开发,因为该技术只是在webpack打包阶段在本地临时生成了node server,来实现类似nginx 的proxy_pass的反向代理效果

proxy工作原理实质上是利用 http-proxy-middleware (opens new window) 这个http代理中间件,实现请求转发给其他服务器。例如:本地主机 A 为 http://localhost:3000 ,该主机浏览器发送一个请求,接口为/api,这个请求的数据(响应)在另外一台服务器 B 地址为 http://10.231.133.22:80上,这时,就可以通过A主机设置webpack proxy,直接将请求发送给B主机。

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

app.use('/api', createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true }));
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

下面我们来举个例子: 新建一个项目,安装 express

cnpm i express -S
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script>
    $.ajax({
      url: "http://localhost:3002/api/list",
      success: function (data) {
        console.log(data);
      }
    });
  </script>
</body>
</html>

配置服务:

// server1.js
const app = require('express')()

app.use(express.static('./'))

app.listen(3000);

后端接口

// server2.js
const express = require('express'),
      app = express()

app.listen(3002, () => {
  console.log('ok')
})

app.get('/api/list', (req, res) => {
  const { callback } = req.query
  const data = {
    code: 0,
    msg: 'ok',
    data: {
      name: 'f'
    }
  }

  res.send(data)
})

终端执行如下命令,启动服务

node server1.js 
node server2.js

可以看到我们的网页访问地址是 http://localhost:3000/,网页中请求的接口地址是 http://localhost:3002/api/list,存在跨域问题,导致接口请求失败。

下面我们来对项目做一些修改,来解决上面的问题。

首先先安装 http-proxy-middleware

cnpm i http-proxy-middleware -D

然后对 server1.js 文件做如下修改:



 

 





// server1.js
const app = require('express')()
const { createProxyMiddleware } = require('http-proxy-middleware');

app.use('/api', createProxyMiddleware({ target: 'http://localhost:3002', changeOrigin: true }));

app.use(express.static('./'))

app.listen(3000);

重新执行 node server1.js,浏览器中访问 http://localhost:3000/,可以看到接口请求成功了。

# nginx反向代理

使用 nginx 反向代理解决跨域,只需要后端配置,不需要前端做什么事。

从 www.baidu.com 向 www.taobao.com 发请求

server: {
  listen 80;
  server_name www.taobao.com;
  location / {
    # 反向代理
    proxy_pass www.baidu.com;
    proxy_cookie_demo www.baidu.com www.taobao.com;
    add_header Access-Control-Allow-Origin www.baidu.com;
    add_header Access-Control-Allow-Credentials true;
  }
}

# 基于postmessage实现跨域处理

<!-- /postmessage/a.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="iframe" style="display: none;" src="http://localhost:3002/b.html" frameborder="0"></iframe>
  <script>
    iframe.onload = function() {
      iframe.contentWindow.postMessage('来自a页面的信息', 'http://localhost:3002/')
    }
    window.onmessage = function(e) {
      console.log(e.data)
    }
  </script>
</body>
</html>
<!-- /postmessage/b.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    window.onmessage = function(e) {
      console.log(e.data)
      e.source.postMessage('来自b页面的信息', e.origin)
    }
  </script>
</body>
</html>
// server1.js
const express = require('express')
const app = express()

app.use(express.static('./'))

app.listen(3001)
// server2.js
const express = require('express')
const app = express()

app.use(express.static('./'))

app.listen(3002)

# websocket

# document.domain + iframe

这种方式只能实现同一个主域名,不同子域名之间的通信,如:a.f.com 和 b.f.com 。

<!-- a.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="iframe" src="" frameborder="0"></iframe>
  <script>
    document.domain = 'f.com'
    var a = 'a'

    // 获取 b 页面中 b 的值
    console.log(frame.contentWindow.b)

  </script>
</body>
</html>
<!-- b.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    document.domain = 'f.com'
    // 获取 a 页面 a 的值
    console.log(window.parent.a)

    var b = 'b'
  </script>
</body>
</html>

# window.name + iframe

a.html 和 proxy.html是同域的都是 http://localhost:3001/,b.html 的域是 http://localhost:3002/

<!-- a.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="iframe" style="display: none;" src="http://localhost:3002/b.html" frameborder="0"></iframe>
  <script>
    let count = 0
    iframe.onload = function() {
      if (!count) {
        // 需要我们将地址指向到同源中才可以
        iframe.src = 'http://localhost:3001/proxy.html'
        count++
        return
      }
      console.log(iframe.contentWindow.name)
    }
  </script>
</body>
</html>
<!-- proxy.html -->
<!-- 这个页面中可以什么内容都没有 -->
<!-- b.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    window.name = 'hello a'
  </script>
</body>
</html>
// server1.js
const express = require('express')
const app = express()

app.use(express.static('./'))

app.listen(3001)
// server2.js
const express = require('express')
const app = express()

app.use(express.static('./'))

app.listen(3002)

# location.hash + iframe

a.html 和 proxy.html 同域,都在 http://localhost:3001/ 下面, b.html 在 http://localhost:3002/ 下面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接js访问来通信。

<!-- a.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="iframe" style="display: none;" src="http://localhost:3002/b.html" frameborder="0"></iframe>
  <script>
    let count = 0
    iframe.onload = function() {
      if (!count) {
        iframe.src = 'http://localhost:3002/b.html#msg=hello'
        count++
        return
      }
    }
    // 开放给 proxy.html 页面的回调方法
    function fn(res) {
      console.log(res)
    }
  </script>
</body>
</html>
<!-- proxy.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    window.onhashchange = function() {
      window.parent.parent.fn(location.hash)
    }
  </script>
</body>
</html>
<!-- b.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="iframe" style="display: none;" src="http://localhost:3001/proxy.html" frameborder="0"></iframe>
  <script>
    // 监听 a.html 传过来的 hash 值,再传给 proxy.html
    window.onhashchange = function() {
      iframe.src = 'http://localhost:3001/proxy.html' + location.hash
    }
  </script>
</body>
</html>

参考: VSCode 安装 code 命令 (opens new window)

浏览器的同源策略 (opens new window)

九种跨域方式实现原理(完整版) (opens new window)