跨域总结:从CORS到Ngnix

23,761 阅读4分钟

前言

前后端数据交互经常会碰到请求跨域 , 什么是跨域 , 以及有哪些跨域方式 , 我觉得我应该记录下来。

一.什么是跨域?

1. 什么是同源策略及其限制内容?

  1. 同源策略是一个安全策略。所谓的同源,指的是协议,域名,端口相同。浏览器处于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。
  2. 同源策略限制的内容有:
  • Cookie , LocalStorage ,IndexedDB等存储性内容。

  • DOM节点

  • AJAX请求发送后,非同源会被浏览器拦截。
    但是有三个标签是允许跨域加载资源:

      <img src=XXX>
      <link href=XXX>
      <script src=XXX>
    

这也就是JSONP的来源了。

2. 常见的跨域场景

当协议,子域名,主域名,端口号中任意一格不相同时,都算作不同源。

note: 特别说明两点:

  • 第一 : 如果是协议和端口造成的跨域问题"前端"是无能为力的。
  • 第二: 在跨域问题上,仅仅是通过"URL的首部"来识别而不会根据域名对应的IP地址是否相同来判断。"URL的首部"可以理解为""协议,域名和端口必须匹配"。
  • 第三: 请求跨域了,那么到底发出去没有? 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了

二.跨域解决方案

1. JSONP

1.1 JSONP原理

利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的JSON数据。JSONP请求一定需要对方的服务器做支持才可以。

1.2 JSONP和AJAX对比 JSONP和AJAX相同,都是客户端向服务端发送请求,从服务端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

1.3 JSONP优缺点 JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

1.4 JSONP的实现流程

  • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)
  • 创建一个 <script src=>标签 ,把那个跨域的API数据接口地址,赋值给script的src, 还要在这个地址中向服务器传递该函数名(可以通过问号传参?callback=show)。
  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show('我不爱你')。
  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

现在我们来自己封装一个JSONP函数

// 封装 JSONP函数
function jsonp({ url, params, callback }) {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        params = JSON.parse(JSON.stringify(params));
        let arrs = [];
        for (let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        arrs.push(`callback=${callback}`);
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
        window[callback] = function (data) {
            resolve(data);
            document.body.removeChild(script);
        }
    })
}
// 前端调用
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        wd: 'I Love you'
    },
    callback: 'show'
}).then(data => {
    console.log(data)
})

// 后端响应
// 这里用到了 express
var express = require('express');
var router = express.Router();
var app = express();
router.get('/say',function(req,res,next) {
 //要响应回去的数据
  let data = {
    username : 'zs',
    password : 123456
  }

  let {wd , callback} = req.query;
  console.log(wd);
  console.log(callback);
  // 调用回调函数 , 并响应
  res.end(`${callback}(${JSON.stringify(data)})`);
})
app.use(router);
app.listen(3000);

2. CORS

CORS需要浏览器和后端同时支持。IE8和 IE9需要通过 XDomainRequest来实现
浏览器会自动进行 CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置CORS和前端没有什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求

只要同时满足以下两个条件,就属于简单请求

条件1 : 使用下列方法之一:

  • GET
  • HEAD
  • POST

条件2 :Content-Type 的值仅限于下列三者之一 :

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;

复杂请求

不符合以上条件的请求就肯定是复杂请求了。复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请,称为"预检"请求,该请求是option方法的 , 通过该请求来知道服务端是否允许跨域请求。
我们用 PUT 向后台请求时, 属于复杂请求,后台需如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
  res.end() 
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

接下来我们看下一个完整请求的例子,并且介绍下CORS请求相关的字段

//index.html
// 前端代码
<script>
    let xhr = new XMLHttpRequest();
    document.cookie = 'name=hw';
    xhr.withCredentials = true; //前端设置是否带 cookie
    xhr.open('PUT','http://localhost:4000/getData',true);
    xhr.setRequestHeader('name','hw');
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4) {
            if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(JSON.parse(xhr.response));
                console.log(xhr.getResponseHeader('name'))
            }
        }
    }
    xhr.send();
</script>
// server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname))
app.listen(3000)

// 后端代码
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(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', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')
    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不做任何处理
    }
  }
  next()
})
app.put('/getData', function(req, res) {
  let data = {
      username : 'zs',
      password : 123456
  }
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一个响应头,后台需设置
  res.end(JSON.stringify(data))
})
app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('he')
})
app.listen(4000)

3. postMessage

postMessage是HTML5 XMLHttRequest Level 2中的 API, 并且为数不多跨域跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档,多窗口,跨域消息传递

API大概是这样的
otherWindow.postMessage(message,targetOrigin,[transfer])
  • message:将要发送到其他window的数据
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)。或者一个URL。在发送消息的时候,如果目标窗口的协议,主机地址或端口这三者的任意一项不匹配 targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和 message同时传递的 Transferable对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

下面,我们来用一个例子来说明任何使用

4. websocket

WebSocket协议本质是一个基于 TCP 的协议 为了建立一个 WebSocket 连接 , 客户端浏览器首先要向服务器发起一个 HTTP 请求 , 这个请求和通常的 HTTP 请求不同 , 包含了一些附加头信息 , 其中附加头信息 "Upgrade:WebSocket" 表明这是一个申请协议升级的 HTTP 请求, 服务器端解析这些附加的头信息然后产生应答信息返回给客户端, 客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

原生WebSocket API使用起来不太方便,我们使用 Socket.io ,它很好地封装了 webSocket接口,提供了更简单,灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
我们再来看一个例子:

//前端
<script>
    // 定义数据
    let Data = {
        username : 'hw',
        password : 456789
    }
    let socket = new WebSocket('ws://localhost:4000');
    socket.onopen = function() {
        socket.send(JSON.stringify(Data)); // 向服务器发送数据
    }
    socket.onmessage = function(e) {
        console.log(JSON.parse(e.data)); // 接收服务器返回的数据
    }
</script>

// 后端
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss =new WebSocket.Server({port:4000})


// 定义数据
let Data = {
    username : 'zs',
    password : 123456
}

wss.on('connection', function(ws) {
    ws.on('message',function(data) {
        console.log(JSON.parse(data))
        ws.send(JSON.stringify(Data))
    })
})

5. node中间件代理(两次跨域)

实现原理: **同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。**代理服务器,需要做以下几个步骤:

  • 接受客户端请求
  • 将请求转发给服务器
  • 拿到服务器响应数据
  • 将响应转发给客户端

我们现在再来看一个例子: 本地文件index.html文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据

//前端
<script>
    function ajax(
        {
            url = '',
            method = 'get',
            headers = {},
            data = ''
        }
    ) {
        return new Promise((resolve,reject) => {
            // 兼容性处理
            var xhr = new XMLHttpRequest();

            xhr.onreadystatechange = function() {
                if(xhr.readyState === 4) {
                    if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
                        try {
                            // 获取数据
                            var response = JSON.parse(xhr.responseText);
                            resolve(response);
                        } catch( e ) {
                            reject(e);
                        }
                    } else {
                        reject(new Error('Request was unsuccessful' + xhr.statusText));
                    }
                }
            }
            xhr.open(method,url,true);
            // 设置头部
            for(let key in headers) {
                xhr.setRequestHeader(key,headers[key]);
            }
            xhr.send(JSON.stringify(data))
        })
    }
    ajax({
        url : 'http://localhost:3000',
        method : 'post',
        headers : {
            'Content-Type':'application/json;charset=utf-8'
        },
        data : {
            username : 'hw',
            password : '789'
        }
    })
    .then((value,code)=>{
        console.log(value)
    })
    .catch(err=>{
        console.log(err)
    })
</script>

// 代理服务器(http://localhost:3000)
const http = require('http');
// 第一步 : 接受客户端请求
const server = http.createServer((request, response) => {
    // 代理服务器,直接和浏览器直接交互,需要设置CORS的首部字段
    response.writeHead(200, {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': 'Content-Type'
    })
    if(request.method === 'options') {
        response.end();
    }    
    // 第二步 : 将请求转发给服务器
const proxyRequest = http
        .request(
            {
                host: '127.0.0.1',
                port: 4000,
                url: '/',
                method: request.method,
                headers: request.headers,
            },
            serverResponse => {
                // 第三步:收到服务器的响应
                var body = ''
                serverResponse.on('data', chunk => {
                  body += chunk
                })
                serverResponse.on('end', () => {
                  console.log('The data is ' + body)
                  // 第四步:将响应结果转发给浏览器
                  response.end(body)
                })
            }
        )
    .end()
})

server.listen(3000, ()=>{
    console.log('The proxyServer is running at http://localhost:3000')
})

//后端服务器
// http://localhost:4000
const http = require('http');
const data = { title : 'zs', password : '123'};
const server = http.createServer((request,response) => {
    if(request.url === '/') {
        response.end(JSON.stringify(data))
    }
})

server.listen(4000,'127.0.0.1',()=> {
    console.log('The server is running at http://127.0.0.1:4000')
})

nginx反向代理

前言

说实话,nginx要熟悉有点花时间,这里我就介绍怎么用nginx跨域好了
这个东西我也不是很会

nginx与node.js

'Nginx是一款轻量级的HTTP服务器,采用事件驱动的异步非阻塞处理方式框架,这让其具有极好的IO性能,时常用于服务端的反向代理与负载均衡'

这似乎与node.js很像啊,但,其实他们都有自己擅长的领域:Nginx更擅长于底层服务端资源的处理(静态资源处理转发,反向代理,负载均衡等),node.js更擅长于上层具体业务逻辑的处理。两者可以实现结合,助力前端开发。

代理服务器

什么是反向代理?互联网应用基本都基于CS基本结构,即 client端和server端。代理其实就是client端和真正的server端之间增加一层提供特定的服务的服务器,即代理服务器。

正向代理

正向代理大家肯定都用过,其实,翻墙工具其实就是一个正向代理工具。它会把浏览器访问墙外服务器server的网页请求,代理到一个可以访问该网站的代理服务器proxy,这个代理服务器proxy把墙外服务器server上的网页内容获取,再转发给客户端。
具体流程如下:

反向代理

反向代理与正向代理相反,先看流程图:

在反向代理中(实际上,这种情况发生在所有大型网站的页面请求中),客户端发送的请求,想要访问server服务器上的内容。但将被发送到一个代理服务器proxy,这个代理服务器将把请求代理到和自己属于同一个LAN下的内部服务器上,而用户真正想获得的内容就存储在这些内部服务器上。
即向外部客户端提供一个统一的代理入口,客户端的请求,都先经过这个proxy服务器,至于在内网真正访问哪台服务器内容,由这个proxy去控制。
概括的说:就是代理服务器和真正server服务器可以直接访问,属于一个LAN(服务器内网);代理对用户是透明的,即无感知的。

为什么要nginx反向代理

原因:

  • 安全及权限。可以看出,使用反向代理后,用户端将无法直接通过请求访问真正的内容服务器,而必须首先通过nginx.可以通过在nginx层上将危险或者没有权限的请求内容过滤,从而保证了服务器的安全。
  • 负载均衡。例如一个网站的内容被部署在若干台服务器上,可以把这些机子看成一个集群,那么nginx可以将接收到的客户端请求"均匀地"分配到这个集群中所有的服务器上(内部模块提供了多种负载均衡算法),从而实现服务器压力的负载均衡。此外,nginx还带有健康检查功能(服务器心跳检查),会定期轮询向集群里的所有服务器发送健康检查请求,来检查集群中是否有服务器处于异常状态,一旦发现某台服务器异常,那么在以后代理进来的客户端请求都不会被发送到该服务器上(直达后面的健康检查发现该服务器恢复正常),从而保证客户端访问的稳定性。

windows下nginx的下载与安装

nginx下载
nginx配置

nginx的一点功能

1. 快速实现简单的访问控制

经常会遇到希望让某些特定的用户的群体(比如公司内网)访问,或者控制某个url不让人访问.

nginx的配置如下:
location / {
    deny 192.168.1.100; // 禁止
    allow 192.168.1.10/200; // 允许
    allow 10.110.50.16;
    deny all; // 禁止所有
}

其实 deny和 allow是 ngx_http_access_module(已内置) 模块中的语法。
采用的是从上到下匹配方式,匹配到就跳出不再继续匹配。
比如说,上述配置的意思是,首先禁止 192.168.1.100访问,然后允许 192.168.1.10 - 200 ip段内的访问 (排除 192.168.1.100 ) ,同时允许 10.110.50.16 这个单独ip 的访问,剩下未匹配的全部禁止访问。

2.解决跨域

在众多的解决跨域方式中,都不可避免的都需要服务端进行支持,使用Nginx可以纯前端解决请求跨域问题。
nginx配置如下:

server
{
    listen 3002;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        #   指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #   预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #   带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #   表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #   表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #   OPTIONS预检命令,预检命令通过时才发送请求
        #   检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}

参考资料

九种跨域请求实现方式
nginx与前端开发

总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方法。
  • JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据
  • 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制
  • 日常工作中,用的比较多的跨域方案还是cors和nginx反向代理

最后

好吧,我承认最后用nginx划水划过的。
这个,nginx学了我好几天我都没有搞懂这怎么弄。
哎,以后在补充吧。