处理跨域:手动实现CORS和JSONP流程

2,458 阅读5分钟

准备

首先明白一件事:
跨域是浏览器的限制

也就是说不是你真的不能访问,而是浏览器出于他自己的种种担心拦截了你的访问。

至于浏览器在担心什么(什么是跨域,为什么会有跨域),参考:百度

其实cors的原理就是,告诉浏览器:"你别担心啦,这个请求我ok的,让他访问吧"。
而jsonp的原理是,使用浏览器不担心的方式去请求:script标签中的src属性(其他带有src属性的标签也可以,比如img)。

先来模拟出跨域的场景,在本地启动一个最简单的node服务,返回查询参数。

// 新建一个server.js文件,当然前提要安装node

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8"
  });
  res.end(queryStr);
});

server.listen(9999);
console.log('server run at 9999');

启动服务

node server.js

先在浏览器里直接访问一下 http://127.0.0.1:9999/?name=无用书生&age=25 看到返回没有问题

然后就在当前页面(你现在正在阅读文章的掘金页面)f12打开控制台,在console里创建一个ajax请求

var xhr = new XMLHttpRequest();
xhr.open('get', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.send();

执行以后就出现跨域报错了

CORS

首先来看一下cors的跨域原理,其实报错里就写的很明白,我们访问的资源没有设置对掘金这个访问源的头。

在cors的规则中,请求分为简单请求和非简单请求,我们上面发送的就是一个简单请求。

关于简单请求和非简单请求,参考:CORS跨域原理解析

对于简单请求

只要在响应头中(response header)指明允许哪些访问源访问就可以了。

在server.js中给响应头添加 Access-Control-Allow-Origin

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8",
    "Access-Control-Allow-Origin": "*"    // * 代表允许所有的源访问
  });
  res.end(queryStr);
});

server.listen(9999);
console.log('server run at 9999');

重启node服务,再试一次

报错没有了,打开network可以看到 Access-Control-Allow-Origin 已经生效,数据也成功获取到

对于非简单请求

将上面请求的方法从get改成put,再次请求 (put方法就属于非简单请求)

var xhr = new XMLHttpRequest();
xhr.open('put', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.send();

可以看到跨域报错又出现了

刚才设置的响应头依然存在,却不起作用了。
另外这里可以看到我们本来发送的是put请求,请求方法那里写的却是options。原因就是对于非简单请求浏览器会先发送一次预检,预检通过才会发送真正的请求,这个options就是预检请求,因为没有通过,所以也就没有发送真正的请求

其实报错中也写的很明白,我们访问的资源没有设置允许对PUT这个方法的访问

在server.js中给响应头添加 Access-Control-Allow-Methods,设置允许put方法的请求

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT",
  });
  res.end(queryStr);
});

server.listen(9999);
console.log('server run at 9999');

重启服务以后再次请求,可以看到跨域报错就消失了

还可以看到依然先进行了一次预检请求,这次预检请求通过了,继续发送了put请求

非简单请求还对请求头的信息有所限制,原理还是一样的,通过Access-Control-Allow-Headers在返回头中设置允许的访问头就ok了,比如

var xhr = new XMLHttpRequest();
xhr.open('put', 'http://127.0.0.1:9999/?name=无用书生&age=25');
xhr.setRequestHeader("X-Corx-Test", "aabbcc");
xhr.send();

设置允许 X-Corx-Test这个请求头

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT",
    "Access-Control-Allow-Headers": "X-Corx-Test"
  });

  res.end(queryStr);
});

server.listen(9999);
console.log('server run at 9999');

JSONP

由于掘金做了csp处理,无法测试jsonp,用百度进行演示。

什么是csp,参考:阮一峰博客

去掉node服务中对cors的配置

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8"
  });

  res.end(queryStr);
});

server.listen(9999);
console.log('server run at 9999');

在百度首页f12打开控制台,这时候如果再使用ajax请求我们的服务又会报跨域的错误

上面说过jsonp的原理就是使用script标签的src不受浏览器跨域限制的原理
在console中创建一个jsonp请求

var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);

执行以后看到这时候就没有报错了

在network中可以看到这是一个成功的get请求,注意jsonp只能发送get请求

这时候请求虽然成功了,还没拿到返回的数据
获取数据的方法就是在前端定义一个接收数据的函数,然后后端返回的js中执行这个函数,并把要返回的数据作为参数传入
比如在前端定义一个叫做 getData 的函数

var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);
function getData(res) {
  console.log(res);
}

在后端返回的内容中调用这个函数,把数据传进去

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
  const query = querystring.parse(req.url.split('?')[1]);
  const queryStr = JSON.stringify(query);
  res.writeHead(200, {
    "Content-type": "text/plain; charset=utf-8",
  });
  let jsonpStr = `getData(${queryStr})`;
  res.end(jsonpStr);
});

server.listen(9999);
console.log('server run at 9999');

重启服务以后,执行前端代码,数据就可以取到了

这个函数名字要前后端约定一致,另外获取完数据以后最好移除一下script标签

var script = document.createElement('script');
script.src = 'http://127.0.0.1:9999/?name=无用书生&age=25';
document.head.appendChild(script);
function getData(res) {
  console.log(res);
  document.head.removeChild(script);
}