让运维人员彻底搞懂的Nginx 跨域 有详细的实验步骤

作者: 西瓜甜
配套视频: https://www.bilibili.com/video/BV1v7411Z7FU

一. 什么是同源策略

历史

Netscape Communications Corporation(最初是Mosaic Communications Corporation)网景是一家独立的美国计算机服务公司,总部位于加利福尼亚州的山景城,然后在弗吉尼亚州的杜勒斯。[2] 它的Netscape Web浏览器曾经一度占主导地位,但在所谓的第一次浏览器大战之后输给了Internet Explorer和其他竞争者,其市场份额从1990年代中期的90%以上下降到2002年的不足1%。 2006. Netscape创建了JavaScript编程语言,这是用于客户端网页脚本的最广泛使用的语言。该公司还开发了SSL,用于在其后继TLS接管之前保护在线通信的安全。

Netscape Navigator 2.0 是网景公司的旗舰产品,是第一个支持JavaScriptgif 动画的浏览器。

在1995年Netscape Navigator 2.02版本中引入了同源策略的概念,目前,所有浏览器都实行这个策略。。

同源策略

在计算中,同源策略(有时缩写为SOP)是Web应用程序安全模型中的重要概念。可见同源策略是为了 Web 的安全出现的产物。

根据该策略,Web浏览器只允许第一个网页中包含的脚本(JS)访问第二个网页中的数据时,两个网页具有相同的来源

何为相同的来源,两个网页的:

  • 协议(http/https)相同

  • 域名(IP)相同

  • 端口 相同

举例来说 假设要某一台服务器提供的一个资源,这个资源的 url 是 http://www.sharkyun.com/api/json

协议是: http:// 域名是: www.sharkyun.com 端口是: 80(默认端口可不写)

那从如下url 的网页访问 http://www.sharkyun.com/api/json 时的情况如下

http://www.sharkyun.com/user/:同源
http://sharkyun.com/dir/other.html:不同源(域名不同)
http://www.qfedu.com/topic/linux/:不同源(域名不同)
https://www.sharkyun.com/dir/other.html:不同源(协议不同)
http://www.sharkyun.com:81/dir/other.html:不同源(端口不同)

浏览器遵循同源策略的目的

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

此策略可防止一个页面上的恶意脚本(JavaScript 语言编写的脚本程序)通过该页面的文档对象模型来访问另一网页上的敏感数据。

image.png

由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

请记住,同源策略仅适用于 JavaScript 脚本,这一点非常重要。

换句话说,同源策略不适用于 HTML 标签,比如:

   <img src="">
   <link rel="stylesheet" type="text/css" href="">
   <script type="text/javascript"></script>
   <iframe src=""></iframe>

这意味着可以一个网站可以通过网页上相应的HTML标签进行跨源访问另外一个网站的诸如图片,CSS和 JS之类的静态资源。

同源策略的限制范围

  • Cookie、LocalStorage 和 IndexDB 无法读取。

    • Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享
  • DOM 无法获得。

    • 如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信
  • AJAX 请求不能发送。

    • 同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

      No 'Access-Control-Allow-Origin' header is present on the requested resource.

接下来,只讨论 AJAX 请求的跨域问题(见下文)。

二、跨域

1 什么是跨域

当从 A 网站的网页代码中 请求访问 B 网站中的数据资源的行为就成为跨域。

image-20200301170733508.png

3 为何会产生跨域

目前主流的假设网站的技术都是采用前后端分离。

前端只负责静态资源的提供,提供此资源的服务器也称为前端服务器

后端只负责动态资源的提供,提供此资源的服务器也称为后端服务器

静态资源包含 html 页面,css 文件,js 文件, 图片等

动态资源就是 数据库中的纯数据。

比如用户的购物车中的商品,或者电商提供的产品的库存数据等。

一个完成的页面需要静态资源和动态资源的组合。

[图片上传中...(qfnz.jpg-f2d824-1583075275684-0)]

通常前端服务器会通过自己静态页面中的 JS 代码向后端服务器请求数据,

之后把请求到的数据,填充到自己的静态页面中,这个过程也可称为渲染。

在次过程中就会产生跨域的行为。

3.1 部署实验环境

192.168.1.37 Centos7 /Docker nginx 前端服务器

192.168.1.38 Centos7 nginx uwsgi 后端服务器

​ (同时具备反向代理功能)

部署 Nginx 不在讨论,相信既然了解跨域的知识了,部署 Nginx 应该不是问题。

下面只说一下每个服务器的配置文件和页面内容

  • 两台 Nginx 的主配置文件 /etc/nginx/nginx.conf 都一致

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
  • 192.168.1.37 的子配置文件 /etc/nginx/conf.d/default.conf

基本按照默认的即可

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
  • 192.168.1.37 添加如下内容到 /usr/share/nginx/html/index.html 文件中,作为网站的首页内容。

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
    
      <title> 欢迎来到 shark yun</title>
      <script
      src="https://code.jquery.com/jquery-3.4.1.min.js"
      integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
      crossorigin="anonymous"></script>
    </head>
    <body>
       <div id=json></div>
       
       <!-- img 标签直接跨域访问静态资源-->
       <img src="http://192.168.1.38/qfnz.jpg">
    </body>
    <script type="text/javascript">
      // AJAX 跨域请求
      $.ajax({
          url: 'http://192.168.1.38/api/json',
          type: 'GET',
          dataType: 'json',
          success: function(res){
              // 转换为字符串
              data=JSON.stringify(res)
            
              // 添加数据到 页面的  div 标签中
              $("#json").text(data);
           },
          error: function(res){
              console.error(res);
              }
      });
    
    </script>
    </html>
    
  • 192.168.1.38 的子配置文件 /etc/nginx/conf.d/default.conf

server {
    listen       80;
    server_name  localhost;

    location / {
         include uwsgi_params;
         uwsgi_pass 127.0.0.1:8000;
    }

    location ~* \.(gif|jpg|jpeg|js)$ {
        root /static;  # 需要创建对应的目录
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
  • 在 192.168.1.38 服务上创建静态资源存放的目录

    mkdir  /static   # 注意和上面配置文件中的一致
    
  • 传输示例图片到 192.168.1.38 服务器的 /static 目录中,并命名为 qfnz.jpg

qfnz.jpg

3.1.1 uwsgi 介绍

uwsgi 可以启动一个提供动态资源的服务器,有相应的监听端口,支持 socket 和 http 形式。

目前支持多种编程语言,这里我们使用 python。

我们之前说动态资源的数据是存放在数据库中的,编程语言可以访问数据库。但是这里不会真的去连接数据库,这里会用假数据代替。不影响理解跨域。

3.1.2 部署 uwsgi

Uwsgi 官方文档

  • 在 192.168.1.38 服务器上执行如下操作

安装依赖软件

yum install epel-release python2-devel python2-pip

使用 pip 安装 uwsgi

pip install uwsgi

创建应用程序目录

mkdir /opt/webapp

进入应用程序目录并创建 应用程序文件 app.py,添加如下内容

[root@localhost webapp]# cat /opt/webapp/app.py
headers=('Content-Type', 'application/json;charset=utf-8')

def application(env, start_response):
    if env['PATH_INFO'] == '/api/json':
        start_response('200 OK', [headers])
        data = '{"name": "shark","age": 18}'
        return [data]

继续在应用程序目录创建 uwsgi 的配置文件 qf-uwsgi.ini,添加如下内容

[root@localhost webapp]# cat /opt/webapp/qf-uwsgi.ini
[uwsgi]
# 监听本地端口 8000
socket = 0.0.0.0:8000

# 进入到应用程序(app)的主目录
chdir = /opt/webapp/

# 指定app 的启动文件
wsgi-file = app.py

#开启 4 个进程
processes = 4

# 每个进程开启 2 个线程
threads = 2
3.2 启动服务
3.2.1 启动 uwsgi

后端服务器上执行

cd /opt/webapp
# 启动 uwsgi
nohup uwsgi qf-uwsgi.ini &

# 启动 nginx
systemctl start nginx

# 检查监听端口
ss -ntal  |grep 80

image-20200301191617779.png

4 解决 AJAX 跨域请求

解决跨域的方法很多,这里近介绍 Nginx 方式。

接下来会已以实际例子来模拟由于跨域访问导致的浏览器报错,之后通过在 Nginx 代理服务器上设置相应的参数来解决跨域。

从而让运维人员搞清楚什么是宽域,运维人员如何在服务端解决跨域。

先来说说解决 AJAX 跨域的解决方法

  • JSONP
  • WebSocket
  • CORS
4.1 JSONP

是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。是最早解决的方法,目前已不常用。

4.2 WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。 也不常用。

4.3 CORS

CORS 是跨源资源共享(Cross-Origin Resource Sharing)的缩写。

它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要使用浏览器的用户参与。

浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

4.4 两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

  • 只要同时满足以下两大条件,就属于简单请求。否则就是非简单请求。
  1. 请求方法是以下三种方法之一:
  • HEAD
  • GET
  • POST
  1. HTTP的头信息不超出以下几种字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

浏览器对这两种请求的处理,是不一样的。

下面仅分析简单请求

4.5 简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

目前可以在任意一个浏览器中输入 http://192.168.1.37

image-20200301205136719.png

之后右键 点击 检查

image-20200301205206154.png

在浏览器下方 点击 Network

image-20200301205317964.png

再次刷新 浏览器, 并点击 json

image-20200301205432152.png

这个 json 资源的请求就是第一次访问 前端服务器时返回的首页中的 JS 代码发送的一次跨域请求。

image-20200301223910842.png

再次点击 Network

并在 右侧窗口的底部 会看到 Request Headers (请求头)

image-20200301205658025.png

会发现 在请求头中有一字段 Origin

image-20200301212117851.png

这个字段的值表明的此次请求是从那发出来的,就是说明这次请求的源是哪儿:协议 + 域名 + 端口

可以看到图片中的源是 :http://192.168.1.37

这个地址正式我们这个页面的服务器地址。

但是此次请求的目标并不是 http://192.168.1.37 而是 192.168.1.38

可以从这个窗口的最上方内容中看到

image-20200301212155998.png

从下图信息中可以看的出来,

这次请求的资源 json 的 url 为:http://192.168.1.38/api/json

域名是192.168.1.38

但是请求头中的 Origin 字段的值是 192.168.1.37

这就是跨源(跨域)访问

image-20200301212716779.png

前面说了, CORS需要浏览器和服务器同时支持。

服务器接收到请求,从请求头中会拿到这个 Origin 的值。

服务器可以根据自己的配置,来确定是否要返回此次请求的数据。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应,此时的响应码可能是 200。也就是不可以从响应的状态码来判断跨域请求是否成功。

image-20200301215816321.png

image-20200301215907033.png

当浏览器接收到服务器的响应信息,查看响应头。

会发现,这个响应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。

image-20200301220200003.png

报错信息如下

image-20200301220243704.png

总结: 浏览器 CORS 跨源请求是否被允许,浏览器判断的是服务器响应头中是否含有 Access-Control-Allow-Origin字段。

4.6 解决简单请求的跨源访问

从上面的总结中可以看出,服务端解决跨域问题的最简单的方法是在服务器的响应头中添加 Access-Control-Allow-Origin 字段。

此时我们可以在后端服务器(192.168.1.38) 中的自配置文件default.conf添加如下内容 :

add_header Access-Control-Allow-Origin *;

允许任何源发送请求

add_header Access-Control-Allow-Origin *;

也可以指定具体的一个源

add_header Access-Control-Allow-Origin http://192.168.1.37

server {
    listen       80;
    server_name  localhost;
    
    # 添加响应头信息
    add_header Access-Control-Allow-Origin *;
    location / {
         include uwsgi_params;
         uwsgi_pass 127.0.0.1:8000;
    }

    location ~* \.(gif|jpg|jpeg|js)$ {
        root /static;  # 需要创建对应的目录
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

接下来重启 nginx 服务

systemctl restart nginx

重启成功后,再次刷新网页

会看到响应头信息

image-20200301224238114.png

页面中也会展示处理响应的数据

image-20200301224351442.png

4.7 响应头信息的说明

如果Origin指定的域名在许可范围内,根据服务器端不同的设置,服务器返回的响应,可能会多出几个头信息字段。

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个** 表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,Access-Control-Expose-Headers: FooBar可以返回FooBar字段的值。

image-20200301170733508.png

(4) Access-Control-Allow-Headers
Access-Control-Allow-Headers 用于 preflight request(预检请求)中,列出了将会在正式请求的 Access-Control-Request-Headers 字段中出现的首部信息。
通常用于允许客户端(一般是前端的 ajax 请求)在请求头中添加的自定义内容。

语法:

Access-Control-Allow-Headers: <header-name>[,<header-name>]*
Access-Control-Allow-Headers: *

* (wildcard)

对于没有凭据的请求(没有 HTTP cookie 或 HTTP 认证信息的请求),值 *仅作为特殊的通配符值。
但在具有凭据的请求中,它被视为没有特殊语义的文字标头名称 *
请注意,Authorization标头不能使用通配符,并且始终需要明确列出。

示例说明:
看到下图中有如下报错现象

在这里插入图片描述

点击任意一个请求,查看请求头都有哪些内容。

在这里插入图片描述

可以看到保存内容是: Request header field app-id is not allowed by Access-Control-Allow-Headers in preflight response.
其含义是,请求头中包含了 app-id 这个字段,在服务端(这里指的是Nginx)Access-Control-Allow-Headers 设置的值中没有,所有不被允许。

在这里插入图片描述


下图是 Nginx 中配置的 Access-Control-Allow-Headers 的内容:

在这里插入图片描述

App-Id 字段添加到 Access-Control-Allow-Headers 的值中:↓

在这里插入图片描述

参考:
MDN Web Docs

相关文章

文章浏览阅读3.7k次,点赞2次,收藏5次。Nginx学习笔记一、N...
文章浏览阅读1.7w次,点赞14次,收藏61次。我们在使用容器的...
文章浏览阅读1.4k次。当用户在访问网站的过程中遇到404错误时...
文章浏览阅读2.7k次。docker 和 docker-compose 部署 nginx+...
文章浏览阅读1.3k次。5:再次启动nginx,可以正常启动,可以...
文章浏览阅读3.1w次,点赞105次,收藏182次。高性能:Nginx ...