资讯专栏INFORMATION COLUMN

全解跨域请求处理办法

wua_wua2012 / 1695人阅读

摘要:跨域访问的处理办法及适用条件适用条件请求的接口需要支持访问这里需要强调的是,不属于的部分,它只是把放入标签中实现的数据传输,不受同源策略限制。

为什么会有跨域问题

我们试想一下以下几种情况:

我们打开了一个天猫并且登录了自己的账号,这时我们再打开一个天猫的商品,我们不需要再进行一次登录就可以直接购买商品,因为这两个网页是同源的,可以共享登录相关的 cookie 或 localStorage 数据;

如果你正在用支付宝或者网银,同时打开了一个不知名的网页,如果这个网页可以访问你支付宝或者网银页面的信息,就会产生严重的安全的问题。如果该未知网站是黑客的工具,那他就可以借此发起 CSRF 攻击了。显然浏览器不允许这样的事情发生;

想必你也有过同时登陆好几个 qq 账号的情况,如果同时打开各自的 qq 空间浏览器会有一个小号模式,也就是另外再打开一个窗口专门用来打开第二个 qq 账号的空间。

为了解决不同域名相互访问数据导致的不安全问题,Netscape提出的一个著名的安全策略——同源策略,它是指同一个“源头”的数据可以自由访问,但不同源的数据相互之间都不能访问。

同源策略

很明显,上述第1个和第3个例子中,不同的天猫商店和 qq 空间属于同源,可以共享登录信息。qq 为了区别不同的 qq 的登录信息,重新打开了一个窗口,因为浏览器的不同窗口是不能共享信息的。而第2个例子中的支付宝、网银、不知名网站之间是非同源的,所以彼此之间无法访问信息,如果你执意想请求数据,会提示异常:

No "Access-Control-Allow-Origin" header is present on the requested resource. Origin "null" is therefore not allowed access.

那么什么是同源的请求呢?同源请求要求被请求资源页面和发出请求页面满足3个相同:

协议相同
host相同
端口相同

简单理解一下:

/*以下两个数据非同源,因为协议不同*/
http://www.abc123.com.cn/item/a.js
https://www.abc123.com.cn/item/a.js

/*以下两个数据非同源,因为域名不同*/
http://www.abc123.com.cn/item/a.js
http://www.abc123.com/item/a.js

/*以下两个数据非同源,因为主机名不同*/
http://www.abc123.com.cn/item/a.js
http://item.abc123.com.cn/item/a.js

/*以下两个数据非同源,因为协议不同*/
http://www.abc123.com.cn/item/a.js
http://www.abc123.com.cn:8080/item/a.js

/* 以下两个数据非同源,域名和 ip 视为不同源
 * 这里应注意,ip和域名替换一样不是同源的
 * 假设www.abc123.com.cn解析后的 ip 是 195.155.200.134
 */
http://www.abc123.com.cn/
http://195.155.200.134/

/*以下两个数据同源*/                               /* 这个是同源的*/
http://www.abc123.com.cn/source/a.html
http://www.abc123.com.cn/item/b.js
HTTP 简单请求和非简单请求

http 请求满足一下条件时称为简单请求,否则是非简单请求:

请求方法是 HEAD,GET,POST 之一

HTTP的头信息不超出以下几种字段:

Accept

Accept-Language

Content-Language

Last-Event-ID

Content-Type

Content-Type 取值仅限于 application/x-www-form-urlencoded, multipart/form-data, text/plain

非简单请求在发送之前会发送一次 OPTION 预请求,如果在跨域操作遇到返回 405(Method Not Allowed) 错误,需要服务端允许 OPTION 请求。

HTTP 跨域访问的处理办法及适用条件 JSOP
适用条件:请求的 GET 接口需要支持 jsonp 访问

这里需要强调的是,jsonp 不属于 Ajax 的部分,它只是把 url 放入 script 标签中实现的数据传输,不受同源策略限制。由于一般库也会把它和 Ajax 封装在一起,由于其和 Ajax 根部不是一回事,所以这里不讨论。下面是一个 jsonp 的例子:

window.jsonpCallback = console.log;
var JSONP = document.createElement("script");
JSONP.src = "http://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13122222222&t=" + Math.random() + "&callback=jsonpCallback";;
document.body.appendChild(JSONP);

后端支持jsonp方式(Nodejs)

var querystring = require("querystring");
var http = require("http");
var server = http.createServer();

server.on("request", function(req, res) {
    var params = qs.parse(req.url.split("?")[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { "Content-Type": "text/javascript" });
    res.write(fn + "(" + JSON.stringify(params) + ")");

    res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");
document.domain
适用条件: host 中仅服务器不同的情况,域名本身应该相同

www.dom.comw1.dom.com 需要同源才能访问,可以将 document.domain 设置为 dom.com 解决该问题

document.domain = "dom.com";

例如,我想开发一个浏览器插件,发现腾讯视频页有个 iframe 其本身的跨域的,无法获取其 iframe 的 DOM 对象。但域名部分相同,可以通过该方法解决.

注:如果你想设置它为完全不同的域名,那肯定会报同源错误的,注意使用范围!

嵌入 iframe
适用条件: host 中仅服务器不同的情况,域名本身应该相同

有了上面的例子就不难理解这个方法了,严格来说这不是一个新的方法,而是上一个方法的延伸。通过设置document.domain, 使同一个域名下不同服务器名的页面可以访问数据,但值得注意的是:这个数据访问不是相互的,外部页面可以访问 iframe 内部的数据,但 iframe 无法不能访问外部的数据。

location.hash
适用条件:iframe 和其宿主页面通信

一个完成的 url 中 # 及后面的部分为 hash, 可以通过修改这个部分完成iframe 的和宿主直接的数据传递,下面演示一下 iframe 页面(B.html)像宿主(A.html)传数据, 反之同理:

// A.html
data = ["book", "map", "shelf", "knife"];
setTimeout(() => {
  location.hash = window.encodeURIComponent(data.join("/"));
}, 1000);

// B.html
window.parent.onhashchange = function (e) {
  var data = window.decodeURIComponent(e.newURL.split("#")[1]).split("/");
  console.log(data);  // ["book", "map", "shelf", "knife"]
}

*注意反向传递数据时应该使用 window.parent.location.hash

window.name
适用条件:宿主页面和 iframe 之间通信

window对象有个name属性,该属性有个特征:即在 window 的生命周期内,窗口载入的所有的页面 (iframe) 都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

这样在 window 中编辑 window.name 就可以在 iframe 中得到,但这个过程缺乏监听,宿主页面(A.html)和 iframe 页面(B.html)相互并不知道对方在什么时候修改该值:

// A.html
setTimeout(() => {
  window.parent.name = "what!";
}, 2000);

// B.html
setTimeout(() => {
  console.log(window.name);   // what!
}, 2500);
postMessage
适用条件:postMessage 是 H5 提出的一个消息互通的机制,解决 iframe 不能消息互通的问题,也可以跨 window 通信,语法如下:
// 在 www.siteA.com 中发出消息
// @message{any} 要发送的数据(注意:老版本浏览器只支持字符串类型)
// @targetOrigin{string} 规定接收数据的域,只有其指定的域才能收到消息,如果为"*"则没用域的限制
// transfer{any} 与 message 一同发送并转移所有权
window.postMessage(message, targetOrigin, [transfer]);

// 在另一个页面接受参数
window.onmessage = console.log;

这里暂不谈论第三个参数,因为你可能一辈子也用不到它。而 targetOrigin 最好不要使用 "*",除非你想让所有页面都收到你的消息。

一种你会用到的场景(iframe):







这一种仅仅是没有了iframe,当你在同一个浏览器窗口同时打开 www.siteA.comwww.siteB.com 两个标签时也可以这样用






反向代理服务器
页面需要访问一些跨域接口,由于代理的存在,在服务器看来请求是不跨域,所以使用各种请求。但需要注意 http 到 https 的兼容问题。

比如当我在一些在线平台开发网站后得到一个页面 www.site-A.com, 而这个页面需要请求我自己的数据服务器data.site-B.com上的数据, 这样同样会产生跨域问题,但是www.site-A.com这个页面是挂在第三方服务器上的,解决这个问题可以采用代理服务器的方法:

var express = require("express");
var request = require("request");
var app = express();

app.use("/api", function(req, res) {
  var url = "http://data.site-B.com/api2" + req.url;
  req.pipe(request(url)).pipe(res);
});
app.use("/", function(req, res) {
  var url = "http://data.site-C.com";
  req.pipe(request(url)).pipe(res);
});

当然还需要同时配置一个 host:

127.0.0.1 local.www.site-B.com

然后访问 local.www.site-B.com 就 OK 了。

CORS
适用条件:CORS 需要服务端支持,且存在一定的兼容性问题(如今你已经可以不考虑,但必要时不要忘了这个"bug")。其通过添加 http 头关键字实现跨域可访问,包括如下头内容:
# www.siteA.com/api 返回相应需要具有如下 http 头字段

Access-Control-Allow-Origin: "http://www.siteB.com"    # 指定域可以请求,通配符"*"(必须)
Access-Control-Allow-Methods: "GET,PUT,POST,DELETE"    # 指定允许的跨域请求方式(必须)
Access-Control-Allow-Headers: "Content-Type"           # 请求中必须包含的 http 头字段
Access-Control-Allow-Credentials: true                 # 配合请求中的 withCredentials 头进行请求验证

通过 express 实现也很简单,在注册路由之前添加:

var cors = require("cors");   // 通过 npm 安装
app.use(cors());

当然你也可以自定义一个中间件:

// 自定义中间件
var cors = function (req, res, next) {
 // 自定义设置跨域需要的响应头。
 res.header("Access-Control-Allow-Origin", "http://www.siteB.com");
 res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
 next();
};

app.use(cors); // 运用跨域的中间件
WebSocket 协议跨域

ws 协议是 H5 中的 web 全双工通信解决方案,常规 http 属于请求相应的过程,在客户端没有请求的情况下,服务端无法给客户端主动推送数据,ws 协议解决了这个问题,但处于安全考虑,其同样有同源策略的限制。

*这里不讨论通过长连接和服务端挂起请求等方法推送数据,本文只讨论跨域。

下面举个例子(依赖socket.io.js):

// 前端部分
socket.on("connect", function() {
  // 监听服务端消息
  socket.on("message", function(msg) {
    console.log("data from server: " + msg);
  });

  // 监听服务端关闭
  socket.on("disconnect", function() {
    console.log("Server socket has closed.");
  });
});

document.getElementById("input").onkeyup = function(e) {
  if(!e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 13)
    socket.send(this.value);
};

// 后端部分(node.js)
var http = require("http");
var socket = require("socket.io");

// 启http服务
var server = http.createServer(function(req, res) {
  res.writeHead(200, {
    "Content-type": "text/html"
  });
  res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

// 监听socket连接
socket.listen(server).on("connection", function(client) {
  // 监听客户端信息
  client.on("message", function(msg) {
    client.send("hello:" + msg);
    console.log("data from client: " + msg);
  });

  // 监听客户端断开
  client.on("disconnect", function() {
    console.log("Client socket has closed.");
  });
});
HTML 标签中的 crossorigin 属性

HTML 中 , 几种不同的跨域方法比较

方法 使用条件 使用条件是否与后端交互 优点 缺点
JSONP 服务端支持 jsonp 请求 兼容所有浏览器 只支持 GET 请求,只能和服务端通信
CORS 服务器相应需要相关投资端支持 方便的错误处理,支持所有http请求类型 存在浏览器兼容性问题(如今可以忽略了)
document.domain 仅需要跨子域发起请求 使用便捷,没有兼容问题 对于完全不同的域名无法使用
postMessage 浏览器不同 window 间通信、 iframe 和其宿主通信 支持浏览器页面间或页面和 iframe 间同行 需要浏览器兼容 H5 接口
window.name iframe 和其宿主通信 简单易操作 数据暴露在全局不安全
location.hash iframe 和其宿主通信 简单易操作 数据在 url 中不安全并且有长度限制
反向代理 - 任何情况都可用 使用比较麻烦,需要自己建立服务
扩展:基于 webpack 的反向代理配置示例

添加 webpack 配置如下:

const config = {
  // ...
  devServer: {
    // ...
    proxy: {
      "/api": {
        target: "https://data.site-B.com/api2",
        changeOrigin: true, // 允许跨域
        secure: false // 允许访问 https
      },
      "/": {
        target: "https://data.site-C.com",
        changeOrigin: true,
        secure: false
      },
    }
  }
};
module.exports = config;
扩展:基于 Nginx 反向代理和CORS配置示例

CORS 配置

location / {
  add_header  Access-Control-Allow-Origin *;
  add_header Access-Control-Allow-Credentials true;
  add_header  Access-Control-Allow-Methods: GET,PUT,POST,DELETE;
}

反向代理配置

server {
    listen  7001;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.B.com:7001;  #反向代理
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98130.html

相关文章

  • 前端20个真正灵魂拷问,吃透这些你就是中级前端工程师 【上篇】

    摘要:还是老规矩,从易到难吧传统的定时器,异步编程等。分配对象时,先是在空间中进行分配。内存泄漏内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 showImg(https://segmentfault.com/img/bVbwkad?w=1286&h=876); 网上参差不弃的面试题,本文由浅入深,让你在...

    mdluo 评论0 收藏0
  • 前端20个真正灵魂拷问,吃透这些你就是中级前端工程师 【上篇】

    摘要:还是老规矩,从易到难吧传统的定时器,异步编程等。分配对象时,先是在空间中进行分配。内存泄漏内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 showImg(https://segmentfault.com/img/bVbwkad?w=1286&h=876); 网上参差不弃的面试题,本文由浅入深,让你在...

    leap_frog 评论0 收藏0
  • Kubernetes中的负载均衡全解

    摘要:负载均衡器只能与和等特定的云服务提供商一起使用,且均衡器的功能根据提供者而定。因为它是作为一个基于的控制器在内部执行,因此对功能的访问相对不受限制不同于外部负载均衡器,它们中的一些可能无法在层面访问。 很多企业在部署容器的时候都会选择Kubernetes作为其容器编排系统。这是对Kubernetes的可靠性,灵活性和特性广泛的肯定。在这篇文章中,我们将对Kubernetes如何处理一个...

    lansheng228 评论0 收藏0

发表评论

0条评论

wua_wua2012

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<