跨域与nginx代理

为什么会出现跨域

跨域的出现是由于浏览器的同源策略限制,换句话说就是跨域只会出现在浏览器环境里,是浏览器进行了限制的。

同源策略

同源策略主要是为了限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port),如下:

我们发起请求的页面http://www.baidu.com

请求url 是否跨域 结果
http://www.baidu.com/aaa.html 成功
https://www.baidu.com 失败,协议不同(http/https)
http://www.baidu.com:8081/aaa.html 失败,端口号不同(80/8081)
http://test.baidu.com:8081/aaa.html 失败,子域名不同(www/test)
http://www.tengxun.com 失败,主域名不同(baidu/tengxun)

跨域解决方法

  1. 主域名相同,更改document.domain
    这种情况只限于上面的主域名相同,子域名不同的情况下,如www.baidu.comtest.baidu.com,二者的父域名都是baidu.com,可以将其设置为当前域的父域,js里添加document.domain=baidu.com实现资源共享。
  2. 跨域通信,postMessage
    我们可以使用postMessage来向指定的页面传递参数
var openWindow = window.open('http://test.com', 'title');
openWindow.postMessage('我是发送过来的消息', 'http://test.com');
http://test.com接收
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
  console.log(event)
}

实用小例子,我们从一个表格页打开一个列表当详情(浏览器新tab打开),对这个详情页进行修改后自动关闭回到表格页,这时这个表格对数据要进行更新:
+ 使用visibilitychange,缺点:不能跨域

visibilitychange,这个是监听页面可见性。
var openWindow = window.open('http://test.com', 'title');
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') {
  // 页面不可见;
}
if (document.visibilityState === 'visible') {
// 页面可见;
  if(openWindow.closed) { // http://test.com关闭
    // 拉取数据
  }
}
});
  • postMessage,缺点:新打开对页面不能刷新
this.openWindow = window.open("http://test.com");
setTimeout(() => {
  this.openWindow.postMessage(
    {
      type: "postMessage",
      text: "asdfasdfasdfasdfasdzvzx"
    },
    "http://test.com"
  );
}, 4000);

http://test.com:
window.addEventListener("message", function(e) {
  if (e.data.type === "postMessage") {
    console.log(e.data.text);
    console.log(e);
    _this.parent = e.source;
  }
});
操作完成后:
this.parent.postMessage(
  {
    type: "postMessage",
    text: "给我刷新"
  },
  "主页url"
);
  1. JSONP
    主要是和后台约定一个回调名字,发送一个带回调的get请求,后台将数据放在约定名字的回调函数传回来,从回调里面拿参数。
  2. CORS
    CORS 是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,所有问题的都有主流的解决方式,如果说跨域最主流的解决方式是什么,那毫无疑问是CORS了。
    CORS实际上是后端设置的,前面说过,跨域是浏览器的限制,我们在发起请求时,浏览器判断请求是否跨域了,如果后台的服务器没有声明哪些源站可以访问资源,那么浏览器就不会发出真正的请求,把它拦截下来,或者跨站请求可以正常发起,但是返回结果被浏览器拦截了。
    总之我们的最终目的是让浏览器知道我们的目标服务器是允许我们发起请求的地址访问的。

    • CORS 预检请求
      用我的说法这叫options嗅探,在进行跨域的复杂时,浏览器会先发出一个options请求来预先检查这个请求是否被服务器允许,允许的话,正式发出实际的请求。所以在跨域请求时我们经常会看见发出一个请求浏览器上展示有两个,当然这是跨域成功的情况下,失败的话就只有options一个。
    • 简单请求与复杂请求
      options的产生是有条件的,也就是上面我们说的复杂请求才会有,那么什么是复杂什么是简单请求呢?
      根据MDN的描述:
      1.使用下列方法之一:
          GET
          HEAD
          POST
      2.规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
          Accept
          Accept-Language
          Content-Language
          Content-Type (需要注意额外的限制)
          DPR
          Downlink
          Save-Data
          Viewport-Width
          Width
      3.Content-Type 的值仅限于下列三者之一:
          text/plain
          multipart/form-data
          application/x-www-form-urlencoded
      4.请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使5.用 XMLHttpRequest.upload 属性访问。
      6.请求中没有使用 ReadableStream 对象。
    

    满足了以上条件就是简单请求,否则的话是复杂请求。所以我们常出现options的场景就是使用post传递josn数据。

好了,明白了CORS的机制,我们知道了这个设置是后端设置的,可以愉快的甩锅给后端了(^_^),但是我们也得了解一下怎么处理。下面我以node讲解一下处理方式(其他语言都大差不差,主要是原理):

以nest这个node框架讲解:
cors.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class Cors implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const origin = req.get('Origin');
    console.log(origin);
    // 判断是不是来自跨域的请求
    if (origin !== undefined) {
      res.set({
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Credentials': true,
        'Access-Control-Allow-Methods': 'GET, HEAD, PUT, PATCH, POST, DELETE',
      });
      // 判断是不是预检请求
      if (req.method === 'OPTIONS') {
        res.set({
          'Access-Control-Allow-Headers':
          'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization',
          'Content-Type': 'text/plain;charset=UTF-8',
          'Access-Control-Max-Age': 1728000,
          'Content-Length': 0,
        });
        res.status(204).end();
        return;
      }
    }
    next();
  }
}
-------------------------------------------------------------------------------
app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsController } from './cats/cats.controller';
import { Cors } from './cors.middleware';

@Module({
  imports: [],
  controllers: [AppController, CatsController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(Cors)
      .forRoutes({ path: 'fetch', method: RequestMethod.ALL });
  }
}

我们这里主要讲怎么处理,我们写了一个cors.middleware.ts的中间件,把它注入到了服务里,每次请求过来时都会经过这个中间件,我们在中间件里获取请求的req信息,为其添加允许跨域的头,判断过来的是options请求就直接返回204,
实际上后端要处理的就两点:
+ 给返回接口的添加跨域头,让浏览器知道我们允许了跨域访问。
+ 处理options请求,返回204,让浏览器的预检请求通过,发出真实请求。
简单介绍一下添加的header吧

Access-Control-Allow-Origin    允许跨域访问的域名(协议+域名+端口),允许所有的'*'
Access-Control-Allow-Credentials   允许cookie传输,启用此项后,上面的域名不能为'*'
Access-Control-Allow-Methods   允许请求方式
Access-Control-Allow-Headers  用于预检请求中,列出了将会在正式请求的Access-Control-Request-Headers 字段中出现的首部信息。
Access-Control-Expose-Headers  如果想要让客户端可以访问的headers信息,可以将它们在此面列出来,如果后端在response添加了headers,查看响应头也有,但前端拿不到headers,那么应该是这里没添加,注:通配符*支持度不高,请写具体值。  

注意:如果在请求头添加了自定义headers头,那么后端设置的Access-Control-Allow-Headers一定要有headers的key值,比如axios设置config.headers["resources-type"] = "pc",那么Access-Control-Request-Headers: 'resources-type',`resources-type`这个一定要有,不然的话正式请求不会发出
  1. nginx代理
    假如后端不处理跨域,让前端来做,要怎么实现呢?
    前面说了,跨域是浏览器的限制,那么我们真正的请求不走浏览器,用服务器向服务器发起请求,把结果转发回来不就没有跨域了吗?就像我们开发时用脚手架的代理一样,这就是除CORS外的另一个主流的做法,那就是走代理转发(不限于nginx代理,比如node项目的代理等等,原理一样)
    好了看看前端如何处理的吧:

    • 假设我们的服务跑在http://xuxinapi.com:8082上的,我们要请求的接口是http://xuxinapi.com:3000/fetch/login,由于端口号不同出现了跨域,我们先把请求路径改一下,全部去掉域名端口号,并在请求的前面加上/api用作我们nginx的匹配路径,改为/api/fetch/login(第一期脚手架的配置有统一请求配置),然后部署前端项目。
    • 这个时候我们请求的路径全部都有/api了,我们修改nginx的配置,添加转发:
    location /api/ {
      proxy_pass http://172.17.0.4:3000/;  // 这里3000后面加不加/的区别是:不加为`http://172.17.0.4:3000/api/fetch/login`,加上为`http://172.17.0.4:3000/fetch/login`,`/api`去不去掉(我这里是内网ip,可以写其他网站的域名和外网ip)
    }
    

    重启nginx,这时候我们的请求带上了/api的都会被nginx代理转发到本地的3000端口去,且请求不会有options预检。

  2. 其他跨域处理
    在开发中除了请求跨域,我们还会遇到很多跨域的问题,比如字体图标跨域,canvas绘制跨域等等,这类的静态文件的跨域问题一般先去找到源站,看能不能设置添加跨域头,如上传阿里云,七牛云的图片,如果源站不能设置跨域头的话就走nginx代理,和上面的请求做法一样。

本文链接:
后端:链接
前端:链接

您也可能喜欢...

3 条回复

  1. Awesome post! Keep up the great work! 🙂

  2. AffiliateLabz说道:

    Great content! Super high-quality! Keep it up! 🙂

  3. Brianjah说道:

    Nice web-site you possess right here.

发表评论

电子邮件地址不会被公开。 必填项已用*标注