Cross-Origin And Cross-Site

同源策略

浏览器安全的基石是 "同源政策"(same-origin policy)。 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。

所谓同源是指 "协议+域名+端口" 三者相同,即便两个不同的域名指向同一个 IP 地址,也非同源。
它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。

同源策略限制以下几种行为:(1) Cookie、LocalStorage 和 IndexDB 无法读取。(2) DOM 无法获得。(3) AJAX 请求不能发送。

在服务器端(项目上线部署完成)不存在跨域问题,跨域是针对浏览器的协议、域名、端口而言的。

JSONP JSON with padding

浏览器同源策略导致网页中无法通过 Ajax 请求进行跨域访问,JSONP 的原理是部分标签的 src 属性不存在跨域请求的限制,可以访问到跨域的脚本。

服务端不返回字符串格式的数据,而是返回注入数据函数调用的脚本。客户端定义服务端返回调用的同名函数。

src 属性不仅用作将函数名带入请求的参数,而且会执行服务端返回的函数调用。

const script = document.createElement("script"), handle = function (data) { console.log(data.code, data.msg); }
script.src = "http://localhost:3000/jsonp-server?callback=handle";
document.body.appendChild(script)
const express = require('express');
const app = express(), port = 3000;
app.get('/jsonp-server', (req,res)=>{
  const { callback } = req.query, data = { code: 200, msg: 'jsonp request success!' }
  res.send(`${callback}(${JSON.stringify(data)})`) // 也可以使用 end => 不加特殊响应头
});
app.listen(port, () => { console.log(`app listen is running at http://localhost:${port}`); });

类似的在 React 中,组件通讯的属性传递一般只能父传子,要是想实现子传父,可以通过父组件提供回调函数来接收数据,将该函数作为属性的值传递给子组件,子组件以 props 调用回调函数。

由于所有的 src 都是资源文件请求,导致 JSONP 只能使用 GET 方法;在路径传参将函数传递给服务器时,会存在 url 劫持的问题。

jQuery 发起 JSONP 请求时必须指定 dataType: 'jsonp',callback=jQueryxxx 参数会被自动携带,jQueryxxx 是随机生成的回调函数名。其实现过程是动态的在 <header> 中创建和移除 <script>,并发起 JSONP 数据请求。

CORS 跨域资源共享

跨域资源共享是一系列决定浏览器是否阻止前端跨域获取资源的响应头组。通过配置接口服务器相关 HTTP 响应头可解除浏览器端跨域访问限制。

根据请求方式与请求头不同,可以分为简单请求预检请求

  • 请求方式是 GET、POST、HEAD 且 HTTP 头部信息不超过上述代码几种字段(无自定义头部字段)则为简单请求。
  • 请求方式为 GET、POST、HEAD 以外的 METHOD 类型、请求头包含自定义头部字段、向服务器发送了 application\json 格式的数据,满足其中任何一种条件都是预检请求。
const express = require("express");
// const cors = require('cors') + app.use(cors()) => 粗暴型
const app = express();
const port = 3001;
// 使用 express + node 时使用如下配置中间件配置头即可
app.use((req, res, next) => {
  // Access-Control-Allow-Origin - 允许访问该资源的外域URL
  res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); // 使用 * 避免只有单个源可以访问, 但是不可以携带 cookie => 保证安全
  res.header( "Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS, HEAD" );
  res.header( // 支持客户端向服务器发送如下的 9 个请求头
    "Access-Control-Allow-Headers",
    "Accept,Accept-Language, Content-Language,DPR,Downlink,Save-Data,Viewport-Width,Width,Content-Type"
  );
  res.header("Access-Control-Allow-Credentials", true); // 是否允许携带资源凭证-cookie
  // 所有 cors 预先发送 OPTIONS 试探性请求 => 用完注释即可
//   req.method === 'OPTIONS' ? res.send('SURPPORT CROSS DOMAIN REQUEST') : res.send("SORRY, you can't cross, somewhere happens error")
  next();
});
app.get("/getcors", function (req, res) { res.send("finished the request!"); });
app.listen(port, () => { console.log(`app listen is running at http://localhost:${port}`); });
<script src="./node_modules/jquery/dist/jquery.min.js"></script>
<script>
  $.ajax({
    method: "OPTIONS",
    url: "http://localhost:3001",
    success: (res) => {
      console.log(res); // SURPPORT CROSS DOMAIN REQUEST
    },
  });
  $.ajax({
    method: "get",
    url: "http://localhost:3001/getcors",
    success: (res) => {
      console.log(res); // finished the request!
    },
  });
</script>

代理服务器

Vue 项目可以在需要跨域时直接请求本地服务器 8080 端口,通过配置代理转发给远程服务器(资源服务器、数据服务器...)获取资源。注意当请求的内容存在同名于 public 目录中的文件,那么不会转发给服务器,而是直接从 public 目录读取。

// App.vue
created () {
  axios.get('/devserver').then(res => console.log(res.data))
  this.testtt()
},
methods: {
  testtt () {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://localhost:8080/getcors', true)
    xhr.send()
    xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.log(xhr.responseText);
    }}
  }
}
// vue.config.js
// vue.config.js 打包后就不起作用. 可选项 vue.config.js 文件就等同于 webpack 的 webpack.config.js . vue-cli3 之后并不会自动创建 vue.config.js , 因为都是需要修改 webpack 的时候才会创建 vue.config.js 进行配置.
module.exports = {
  devServer: {
    host: "localhost",
    // 项目开启端口
    port: 8080,
    proxy: {
      // /getcors 表示拦截以 getcors 开头的地址
      "/getcors": {
        target: "http://localhost:3001",
        changeOrigin: true, // 是否如实禀报服务器 - 后端通过request.get('Host')查看
      },
      "/devserver": {
        target: "http://localhost:3001",
        changeOrigin: true,
      },
      "/needrewrite": {
        target: "http://localhost:3002",
        changeOrigin: true,
        // 匹配以 needrewrite 开头的路径转为空字符串.
        // 避免携带控制服务器作用的 needrewrite,被携带到请求路径中
        pathRewrite: { "^/needrewrite": "" },
        ws: true, // 用于支持 websockect
      },
    },
  },
};
// node.js 后端服务
const express = require('express');
const app = express();
const port = 3001;
app.get('/getcors',function (req, res) {
  res.send('finished the request!')
});
app.get('/devserver',function (req, res) {
  res.send('finished the request by devserver!')
});
app.listen(port, () => {
  console.log(`app listen is running at http://localhost:${port}`);
})
// 核心原理 => webpack 的 devServer.proxy 使用的 http-proxy-middleware 包
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware')
const app = express();
const port = 8080;
app.use(
  "/",
  createProxyMiddleware({
    target: "http://localhost:3001",
    changeOrigin: true
  })
)

app.listen(port, () => { console.log(`app listen is running at http://localhost:${port}`); })

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!