前后端通信及同源策略、跨域实现的方案


外圆内方 2019-3-26 js 跨域 代理

前后端通信及同源策略、跨域实现的方案

  1. 同源策略
  2. 前后端通信的几种方式
  3. 补充的代理方案

一、同源策略

  1. 同源策略的含义

    • 协议相同
    • 域名相同
    • 端口相同

以https://www.waiyuanneifang.top/index.html为例:

URL 结果 原因
http://www.waiyuanneifang.top/index.html 失败 不同协议
https://www.waiyuanneifang.top:8080/index.html 失败 不同端口
https://www.wai.top/index.html 失败 不同域名
  1. 同源策略的目的及限制

目的:

  同源策略限制从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。举个例子:比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码。

限制:

  1. 页面中的链接,重定向以及表单提交不会受到同源策略限制。
  2. script,img,link,iframe 都可以加载跨域资源
  3. javascript 不能操作 Cookie、LocalStorage 和 IndexDB,无法获取 DOM,不能发送 Ajax 请求

二、前后端通信的几种方式

  1. Ajax(同源)
function Ajax (options) {
    var xhr = XMLHttpRequest ? new XMLHttpRequest() || new window.ActiveXObject("Microsoft")
    	var data = options.data,
             url = options.url,
             type = options.type,
             dataArr = []
    for (var k in data) {
        dataArr.push(k + '=' + data[k])
    }
    if (toUpperCase(type) === 'GET') {
        url = url + '?' + dataArr.join('&')
        xhr.open(type, url.replace(/\?$/g, ''), true)
        xhr.send()
    }
    if (toUpperCase(type) === 'POST') {
        xhr.open(type, url, true)
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded")
        xhr.send(dataArr.join('&'))
    }
    xhr.onload = function () {
        if ( (xhr.status === 200 || xhr.status === 304) && xhr.readyState === 4 ) {
            if (options.success && typeof options.success === 'function') {
                options.success.call(xhr, JSON.parse(xhr.responseText))
		 }
        } else {
		if (options.error && typeof options.error === 'function') {
		    options.error.call(xhr, undefined))
		}
    }
}

  1. Jsonp(跨域):通过 script 标签可加载跨域资源实现
function jsonp(url, callbackName, onsuccess, onerror, charset) {
	window[callbackName] = function() {
		if (onsuccess && typeof onsuccess === "function") {
			onsuccess(arguments[0])
		}
	}
	var script = document.createElement("script")
	script.setAttribute("type", "text/javascript")
	charset && script.setAttribute("charset", charset)
	script.setAttribute("src", url + "&callback=" + callbackName)
	script.onload = script.onreadystatechange = function() {
		if (!script.readyState || /loading|complete/.test(script.readyState)) {
			script.onload = script.onreadystatechange = null
			if (script.parentNode) {
				script.parentNode.removeChild(script)
			}
			window[callbackName] = null
		}
	}
	script.onerror = function() {
		if (onerror && typeof onerror === "function") {
			onerror()
		}
	}
	document.getElementsByTagName("head")[0].appendChild(script)
}

注:jsonp 是以 get 方式请求的

  1. Hash(跨域):URL 中#后面的内容,改变不会触发页面
// 页面A中发送data
function hash(data) {
	var B = document.getElementsByTagName("iframe")
	B.src = B.src + "#" + JSON.stringify(data)
}
// 页面B接收数据
window.onhashchange = function() {
	var data = window.location.hash
}
  1. WebSocket(不受同源限制):服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,属于服务器推送技术的一种。

    • 建立在 TCP 协议之上,服务器端的实现比较容易。
    • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
    • 数据格式比较轻量,性能开销小,通信高效。
    • 可以发送文本,也可以发送二进制数据。
    • 没有同源限制,客户端可以与任意服务器通信。
    • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
var ws = new WebSocket("wss://echo.websocket.org")
// 连接成功
ws.onopen = function() {
	console.log("open!")
}
// 向服务器发送
ws.send("your message")
// 接收服务器推送
ws.onmessage = function(e) {
	console.log("message!")
}
// 连接关闭
ws.onclose = function(e) {
	console.log("closed!")
}
  1. CORS(跨域资源共享):是一个 W3C 标准,需要服务器和客户端都支持,其原理在于使用自定义的 HTTP 头部允许浏览器和服务器相互了解对方,从而决定请求是否成功,分为简单请求和非简单请求

简单请求: 服务端返回后浏览器查看响应中是否包含 Access-Control-Allow-Origin,如果 Access-Control-Allow-Origin 存在并且包含了当前页面所在的域,那么浏览器将不再根据同源策略对该数据做访问限制

非简单请求: 与简单请求不同。

  • 使用了任一 PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 为 HTTP 方法。
  • 人为设置了 Accept, Accept-Language,Content-Language,DPR,Downlink,Save-Data,Viewport-Width,width。
  • Content-Type 为 application/x-www-form-urlencoded、multipart/form-data、text/plain 以外的值。
  • 请求中 XMLHttpRequesUpload 对象注册了任意多个事件监听器。
  • 请求中使用了 ReadableStream 对象。

  满足上面任一一条,都会先使用 OPTIONS 方法发起一个预检请求到服务器,已获知服务器是否允许该实际请求。可以避免对服务器的用户数据产生未预期的影响。

注:CORS 对比 JSONP,都能解决 Ajax 直接请求普通文件存在跨域无权限访问的问题,区别在于:
1. JSONP 只能实现 GET 请求,而 CORS 支持所有类型的 HTTP 请求
2. 使用 CORS,开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据,比起 JSONP 有更好的错误处理。
3. JSONP 主要被老的浏览器支持,它们往往不支持 CORS,而绝大多数现代浏览器都已经支持了 CORS

详细看阮一峰老师的博客

  1. EventSource(服务器端推送,不支持跨域,IE 完全不支持)
// 客户端,传入接口url作为参数
var evtSource = new EventSource("http://localhost/api")
// 监听错误
evtSource.onerror = function() {
	console.log("error!")
}
// 监听推送消息
evtSource.onmessage = function() {
	console.log("message!")
}
// 连接打开
evtSource.onopen = function() {
	console.log("open!")
}
  1. navigator.sendBeacon(用于通过 HTTP 异步传输少量数据到 Web 服务器)

 参数一:data 将要发送到的 url
 参数二:发送给服务器的 ArrayBufferView, Blob, DOMString, 和 FormData 类型的数据

 在页面 unload 时,如果要上报当前数据,采用 xhr 的同步上报方式,会阻塞当前页面的跳转;使用 new Image 有可能遇到 aborted,导致无法成功发送。使用 sendBeacon 则不会存在上述问题。sendBeacon 如果成功进入浏览器的发送队列后,会返回 true;如果受到队列总数、数据大小的限制后,会返回 false。返回 ture 后,只是表示进入了发送队列,浏览器会尽力保证发送成功,但是否成功了,不会再有任何返回值。目前暂无具体的数据长度限制标准。Google Analytics 使用 sendBeacon。

考虑到对目前浏览器的支持情况,需要做一下降级支持

navigator.sendBeacon ||
	new Function(
		'var xhr=new XMLHttpRequest();xhr.open("POST",arguments[0],true);r.send(arguments[1]);'
	)
  1. postMessage(跨域):H5 标准,可以和其打开的新窗口,多窗口之间,嵌套的 iframe 进行异步的数据传递。

 postMessage()有两个参数。第一个是 data,因兼容性问题最好采用字符串。第二个是 origin,指明目 标窗口的源,协议+主机+端口号[+URL],URL 会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将 message 传递给指定窗口,当然如果愿意也可以设置为"*",传递给任意窗口,如果要和当前窗口同源的话设置为"/"。

// 在A(http://A.com)页面向B(http://B.com)发送数据
window.postMessage("data", "http://B.com")
// 在B中监听数据改变
window.addEventListener("message", function(e) {
	console.log(e.data)
	console.log(e.source)
	console.log(e.origin)
})

三、补充的代理方案

  1. 通过 nginx 进行反向代理
http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;
		location / {
            root D:/dist;
            index  index.html;
		}

        location  ~ ^(/api) {
            # 需要代理到的地址
            proxy_pass http://127.0.0.1:8021;
            add_header 'Access-Control-Allow-Origin' '*';
        }
    }
}
  1. WebpackDevServer 配置 proxy

  原理是 http-proxy-middleware 通过引入 node 的 http-proxy 插件起了一个小型的反向代理的服务器(类似于 nginx),把目标地址 https://www.waiyuanneifang.top 的请求做处理,先解析 Host(Header)、HTTP 信息等,然后构造出目标服务器 http 报文,请求目标服务器并收到响应。把返回的响应返回请求源。

module.exports = {
	devServer: {
		proxy: {
			"/api": {
				target: "https://www.waiyuanneifang.top",
				pathRewrite: { "^/api": "" }
			}
		}
	}
}

以上都是查阅资料及个人理解所整理,若有不对的地方请留言指正,谢谢!