浏览器跨域问题及几种常见解决方案:CORS,JSONP,Node代理,Nginx反向代理

什么是同源策略

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

MDN上有详细解释, 如果两个 URL 的协议端口(如果有指定的话)和主机都相同的话,则这两个 URL 是同源 的。 比如:跟URL http://store.company.com/dir/page.html 的源进行对比,这个源默认端口是80。

浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作 。 同源策略控制不同源之间的交互,例如在使用 XMLHttpRequest<img> 标签时则会受到同源策略的约束。这些交互通常分为三类:

1、跨源写操作 (Cross-origin writes)一般是被允许的,如链接links重定向以及表单提交(如form表单的提交)

2、跨源资源嵌入 (Cross-origin embedding)一般是被允许的,如 imgscript 标签

3、跨源读操作 (Cross-origin reads)一般是不被允许的,如我们的AJAX接口请求

什么是跨域问题

浏览器出于安全考虑,对于同源的请求通过,对于不是同源 的请求会限制,这就是浏览器的 同源策略,对于不满足浏览器浏览器同源策略出现的开发问题,就是跨域问题。

说得更直白一点,我们通过一个url访问一个页面,这个页面的url叫做页面源 ,在这个页面中会有很多的请求,比如请求css,js,图片等等,还有使用xhr或者fetch去请求一些动态数据,这些请求的都url地址叫做目标源 ,当页面源和目标源的协议、主机、端口不一致,就会出现跨域问题。本文重点关注使用AJAX发请求遇到的跨域问题。

注意:当跨域请求时,浏览器同样会发出请求,服务器也同样会响应,但是当浏览器接受到服务器的响应时会去校验,校验不通过,就会出现跨域报错。

比如在百度页面里请求淘宝,跨域报错了,但是淘宝服务器是成功响应了的。

解决方案

CORS

是W3C的标准,是AJAX跨域请求的根本解决方式。在 Node代理 和 Nginx反向代理中一些情况也离不开CORS

MDN上面 跨源资源共享(CORS)和阮一峰老师的 跨域资源共享 CORS 详解 做了详细的解释 ,CORS是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。它使浏览器通过对跨域请求的校验。使用这种方法,关键在于服务器。

区分简单请求和非简单请求(预检请求)

CORS把请求分成两种:简单请求和预检请求。

满足下面所有条件的是简单请求:

1、请求方法是:get,head,post;

3、请求头信息不超过以下几个字段 Accept,Accept-Language,Content-Language,Content-Type, 不要去改其他请求头字段;

2、Content-type类型只能是: text/plainmultipart/form-dataapplication/x-www-form-urlencoded

4、如果请求是使用 XMLHttpRequest 对象发出的,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何事件监听器;也就是说,给定一个 XMLHttpRequest 实例 xhr,没有调用 xhr.upload.addEventListener(),以监听该上传请求。

5、请求中没有使用 ReadableStream 对象。

简单请求以外的就是非简单请求。

例如:

在淘宝页面请求百度页面,使用put方法,非简单请求,method:put + preflight

在淘宝页面请求百度页面,修改请求头,非简单请求,method:get + preflight

在淘宝页面请求百度页面,修改content-type,非简单请求,method:post + preflight

浏览器发送简单请求

浏览器发现跨域是个简单请求,会带上一个请求头OriginOrigin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

服务器处理简单请求

要解决跨域,服务器只需要设置对应到响应头:

Access-Control-Allow-Origin

必须设置的字段 ,可以设置*,允许所有源访问,或者设置明确允许的源,但是就不能发送cookie了,需要明确设置为允许通过的源

Access-Control-Allow-Credentials

在跨域请求下,浏览器是不带 Cookie 的。但是客户端可以通过设置 withCredentials 来进行传递 Cookie

js 复制代码
// xhr
const xhr = new XHRHttpRequest();
xhr.withCredentials = true;

// Axios 
axios.default.withCredentials = true;

// fetch
fetch(url, {credentials: 'include'});

服务端设置

js 复制代码
Access-Control-Allow-Credentials:true
Access-Control-Allow-Origin:非 *****

服务端实现测试:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CORS简单请求</title>
  </head>
  <body>
    <script>
      fetch("http://localhost:2023/cross");
    </script>
  </body>
</html>
js 复制代码
const Koa = require("koa");
const koaRouter = require("@koa/router");

const app = new Koa();
const router = new koaRouter();

router.get("/cross", (ctx, next) => {
  ctx.body = {
    message: "跨域简单get请求成功"
  };
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(2023, () => console.log("koa服务器启动"));

把html用本地或者live server方式打开,然后请求koa起的服务器地址,会出现跨域问题,不同源

给响应头设置Access-Control-Allow-Origin:* 就请求成功了,

js 复制代码
const Koa = require("koa");
const koaRouter = require("@koa/router");

const app = new Koa();
const router = new koaRouter();

app.use(async (ctx, next) => {
  // ctx.set("Access-Control-Allow-Origin", "*");
  // 或者
  ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
  ctx.set("Access-Control-Allow-Credentials", true);
  await next();
});

router.get("/cross", (ctx, next) => {
  ctx.body = {
    message: "跨域简单get请求成功",
  };
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(2023, () => console.log("koa服务器启动"));

浏览器发送非简单请求

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight):

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的Ajax请求,否则就报错。

"预检"请求用的请求方法是OPTIONS,预检请求中,请求头字段:

Origin,必带的字段;

Access-Control-Request-Method ,必带的字段,用来列出浏览器的CORS请求用到的HTTP方法;

Access-Control-Request-Headers:是一个逗号分隔的字符串,实际请求将携带自定义请求头字段

例如:

put请求,请求头中没有Access-Control-Request-Headers

get请求,header中加自定义字段,请求头中有Access-Control-Request-MethodAccess-Control-Request-Headers

put请求,修改content-type,请求头中有Access-Control-Request-MethodAccess-Control-Request-Headers

服务器处理非简单请求

响应头字段:

Access-Control-Allow-Origin,必须设置的字段,允许通过预检的源

Access-Control-Request-Method,也是必须设置的字段,服务器允许请求头头中携带字段

Access-Control-Request-Headers,服务器将接受后续的实际请求方法

Access-Control-Max-Age,减少预请求的次数,需要包含在预请求的响应头中,指定在该时间内预请求验证有效,不必每次都进行预请求,它的单位是 s。如 Access-Control-Max-Age: 86400,即有效期为1天

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CORS简单请求</title>
  </head>
  <body>
    <script>
      fetch("http://localhost:2023/cross", {
        method: "put",
        headers: {
          "content-type": "application/json",
          "my-header": "123",
        },
      });
    </script>
  </body>
</html>
 </script>
 

Node原生实现:

js 复制代码
const Koa = require("koa");
const koaRouter = require("@koa/router");

const app = new Koa();
const router = new koaRouter();

app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
  ctx.set("Access-Control-Allow-Headers", "Content-type,My-header");
  ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set("Access-Control-Allow-Credentials", true);
  ctx.set("Access-Control-Max-Age", 86400);

  await next();
});

router.put("/cross", (ctx, next) => {
  ctx.body = {
    message: "跨域put请求成功",
  };
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(2023, () => console.log("koa服务器启动"));

koa中间件实现

js 复制代码
const Koa = require("koa");
const koaRouter = require("@koa/router");
const cors = require("koa2-cors");

const app = new Koa();
const router = new koaRouter();

app.use(
  cors({
    origin: function (ctx) {
      // 允许的源
      return ctx.headers.origin;
    },
    // 指定本次预检请求的有效期,单位为秒
    maxAge: 500000,
    // 是否允许发送 Cookie
    credentials: true,
    // 设置所允许的 HTTP 请求方法
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    // 设置服务器支持的所有头信息字段
    allowHeaders: ["Content-Type", "My-header"],
  })
);

router.put("/cross", (ctx, next) => {
  ctx.body = {
    message: "跨域put请求成功",
  };
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(2023, () => console.log("koa服务器启动"));

JSONP

出现的早,兼容性好(兼容低版本IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。

缺点是只支持 GET 请求,不支持 POST 请求。

跨源资源嵌入 (Cross-origin embedding)一般是被允许的,动态创建 <script> 脚本标签,通过跨域脚本嵌入不受同源策略限制的方法实现请求第三方服务器数据内容。除了适用于 <script> 脚本标签,HTML 中包含 srchref 属性的标签均不受同源策略限制。

所以JSONP的做法是, 把前端定义好的一个函数名,放在src路径上,当作参数传递给服务端,然后服务端拿到这个函数名, 拼成一个函数调用字符串返回前端,并把响应结果当作参数传递给前端:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JSONP请求</title>
  </head>
  <body>
    <script>
      // 动态创建脚本标签
      const script = document.createElement("script");

      // 设置接口地址
      script.src = "http://localhost:2023/cross?callback=jsonpCallback";

      // 插入页面
      document.body.appendChild(script);

      // 通过定义回调函数接收响应数据
      function jsonpCallback(res) {
        console.log(res);
        // todo
      }
    </script>
  </body>
</html>

服务端实现

js 复制代码
const Koa = require("koa");
const koaRouter = require("@koa/router");

const app = new Koa();
const router = new koaRouter();

router.get("/cross", (ctx, next) => {
  // 拿到前端传递的函数名
  const { callback } = ctx.query;
  // 返回一个函数调用的字符串
  ctx.body = `${callback}(${JSON.stringify({
    msg: "jsonp跨域请求成功",
    code: 200,
  })})`;
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(2023, () => console.log("koa服务器启动"));

Node代理

CORSJSONP 都需要服务器的配合,CORS需要服务器设置响应头,JSONP需要服务器返回一段函数调用的Js代码,使用Node代理就不需要后端的配合。

代理服务器设置CORS

使用koa起一个目标服务器

js 复制代码
// 目标服务器 target.js

const Koa = require("koa");
const KoaRouter = require("@koa/router");

const app = new Koa();
const router = new KoaRouter({ prefix: "/cross" });

router.get("/", (ctx) => {
  ctx.body = {
    msg: "node代理请求成功",
    code: 200,
  };
});

app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa启动"));

客户端请求,使用 live server或者本地打开

html 复制代码
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Node代理</title>
  </head>
  <body>
    <button id="btn">Node代理请求</button>
    <script>
      const btnEL = document.querySelector("#btn");

      btnEL.onclick = function () {
        fetch("http://localhost:2023/cross");
      };
    </script>
  </body>
</html>

此时必然出现跨域问题

再使用express起一个代理服务器,使用一个中间件 http-proxy-middleware,当我们在webpack中,配置proxy时,底层也是使用的 http-proxy-middleware

js 复制代码
// 代理服务器 proxy.js

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

const app = express();

app.use(
  "/cross",
  createProxyMiddleware({
    // 要代理的目标地址
    target: "http://localhost:2023",
    changeOrigin: true,
    onProxyRes(proxyRes, req, res) {
     // CORS解决跨域
      res.header("Access-Control-Allow-Origin", req.headers.origin);
    },
  })
);

app.listen(2024, () => {
  console.log("express代理服务器启动");
});

再修改一下客户端请求地址为代理服务器地址

js 复制代码
  btnEL.onclick = function () {
    fetch("http://localhost:2024/cross");
  };

然后请求成功

部署静态资源服务器

我们平时开发中使用webpack和vite等构建工具,用命令 npm run serve ,npm run dev开启一个本地服务器,然后把我们在src文件夹的源代码部署在本地服务器。在浏览器中打开项目,页面源和目标源都是代理服务器地址。

例如:

客户端代码,目标服务器代码都同上,将index.html放到public文件夹

修改一下代理服务器代码

js 复制代码
// 代理服务器 proxy.js

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

const app = express();
//部署静态资源
app.use(express.static(path.resolve(__dirname, "./public")));

app.use(
  "/cross",
  createProxyMiddleware({
    // 要代理的目标地址
    target: "http://localhost:2023",
    changeOrigin: true,
  })
);

app.listen(2024, () => {
  console.log("express代理服务器启动");
});

Nginx反向代理

什么是正向代理?Node代理就是正向代理,我们主动的访问代理服务器地址,目标服务器不知道真实的客户端(隐藏真实客户端

反向代理就是客户端什么都不用配置,甚至都不知道自己的请求被代理了,被动的代理,以为自己访问的就是真实的服务器(隐藏真实的服务器)。

下载 nginx,运行后,在地址栏输入localhost,默认80端口, 可以看到启动成功的页面

打开下载的文件\conf\nginx.conf,可以看到

conf 复制代码
    server {
        #上图访问的就是这个80服务
        listen       80;
        server_name  localhost;
        
        # 上图显示的就是这个index.html文件
        location / {
            root   html;
            index  index.html index.htm;
        }
    }

启动一个koa服务器

js 复制代码
const Koa = require("koa");
const KoaRouter = require("@koa/router");

const app = new Koa();
const router = new KoaRouter({ prefix: "/cross" });

router.get("/", (ctx) => {
  ctx.body = {
    msg: "node代理请求成功",
    code: 200,
  };
});

app.use(router.routes()).use(router.allowedMethods());
app.listen(2023, () => console.log("koa目标服务器启动"));

配置nginx

conf 复制代码
 server {
        #上图访问的就是这个80服务
        listen       80;
        server_name  localhost;
        
        # 上图显示的就是这个index.html文件
        location / {
            root   html;
            index  index.html index.htm;
        }
        
        # 代理的路径
        location /cross {          
            proxy_pass  http://localhost:2023;
        }
    }

客户端发送请求

html 复制代码
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Nginx反向代理</title>
  </head>
  <body>
    <button id="btn">Nginx反向代理</button>
    <script>
      const btnEL = document.querySelector("#btn");
      
      btnEL.onclick = function () {
        fetch("http://localhost:80/cross");
      };
    </script>
  </body>
</html>

没有设置CORS,出现跨域问题

修改一下nginx配置,设置CORS,同样,需要根据简单和非简单请求设置不同的响应头

conf 复制代码
 server {
        #监听80端口
        listen       80;
        server_name  localhost;
        
        # 设置CORS
        # 指定响应资源是否允许与给定的 origin 共享
        add_header Access-Control-Allow-Origin '*';
        add_header Access-Control-Allow-Credentials 'true';
        # 配置允许跨域的请求方法
        add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,Options';
        # 配置允许跨域的请求头
        # add_header Access-Control-Allow-Headers 'Authorization,Content-Type,Accept,Origin,User-Agent,Cache-Control,X-Mx-ReqToken,X-Requested-With';
        
        # 处理非简单请求 ,第一次预检 Preflight(method: OPTIONS)
        if ($request_method = 'OPTIONS') {
            return 200;
        }
        
        # 代理的路径
        location /cross {          
            proxy_pass  http://localhost:2023;
        }
    }

请求成功

参考:

跨域资源共享 CORS 详解

JSONP 跨域原理及实现

JavaScript Guidebook

深入理解 http 反向代理(nginx)

相关推荐
她似晚风般温柔7892 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
王中阳Go2 小时前
字节跳动的微服务独家面经
微服务·面试·golang
Jiaberrr3 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy3 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白3 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、3 小时前
Web Worker 简单使用
前端
web_learning_3213 小时前
信息收集常用指令
前端·搜索引擎
Ylucius3 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
tabzzz3 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百3 小时前
Vuex详解
前端·javascript·vue.js