熟练掌握“跨域”问题的各种解决方案
是每位前端工程师的基本素养
1. 跨域基本原理
1.1. 同源策略(Same origin policy)
1.2. 同源策略限制了什么?
1.3. 解决方案?
2. JSONP 跨域
3. postMessage 跨域
4. iframe 跨域
4.1. iframe + iframe
4.2. iframe + location.hash
5. CORS 跨域
5.1. 定义
5.2. 两种请求
5.3. 简单请求
5.4. 预检请求
5.5. 身份凭证问题(Credentials)
5.6. 响应头获取问题(response->headers)
5.7. CORS 相关 HTTP 头部字段
6. Nginx 跨域
7. Websocket 跨域
1. 跨域基本原理
1.1. 同源策略(Same origin policy)
同源策略(same-origin policy)是一个重要的安全策略。它用于限制从一个源(origin)加载的文档或脚本,如何与另一个源(origin)的资源进行交互。
1.2. 同源策略限制了什么?
示例1:跨域场景下,浏览器限制脚本API的访问。
page1.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Page1</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<button onclick="callPage2()">Call function in Page2!</button>
<div>
<iframe id="page2"
src="http://192.168.56.1:8180/cross-orgin/page2.jsp"
style="width:500px;"></iframe>
</div>
<script>
function callPage2(){
var page2 = document.getElementById("page2");
page2.contentWindow.fnInPage2();
}
</script>
</body>
</html>
page2.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Page2</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<script>
function fnInPage2(){
alert("function in Page2 invoked!");
}
</script>
</body>
</html>
示例2:IE浏览器,跨域检查,不考虑端口。
1.3. 解决方案?
一个域下的文档或脚本试图去请求另一个域下的资源,这被称作为广义上跨域。我们经常讨论的跨域是从狭义角度去理解,即:由浏览器同源策略限制的一类请求场景。有以下几个常用的解决方案:
2. JSONP 跨域
2.1. 原理
JSONP 利用了 <script> 标签可以加载跨域资源的特性,通过JavasScript Callback的形式实现跨域访问。具体来说,就是在 DOM 中通过动态创建 <script> 标签,并给标签设置 src 属性,在访问请求参数中传递需要回调的函数名;同时,服务端在响应 JSONP 请求时,将数据作为请求参数指定的客户端回调函数参数作为返回值。
2.2. 特性
优点:
缺点:
2.3. 示例
jsonp.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript">
function jsonp(url, params, callbackName) {
var src = url + "?callbackName=" + callbackName;
for(var key in params){
var val = encodeURIComponent(params[key]);
src += ("&" + key + "=" + val);
}
var script = document.createElement("script");
script.src = src;
document.getElementsByTagName("head")[0].appendChild(script);
}
function mycallback(params){
alert(params);
}
jsonp("http://192.168.56.1:8180/cross-orgin/HandleJSONP", {
xm: "John",
age: 18,
idno: "37020320081206129X"
}, "mycallback");
</script>
</body></html>
jsonp-jquery.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript" src="jquery-1.12.4.js"></script>
<script type="text/javascript">
function mycallback(params){
alert(params);
}
$.ajax({
url: 'http://192.168.56.1:8180/cross-orgin/HandleJSONP',
dataType: "jsonp",
data: {
xm: "John",
age: 18,
idno: "37020320081206129X"
},
jsonp: "callbackName",
jsonpCallback: "mycallback"
})
</script>
</body></html>
HandleJSONP.java:
package webj2ee;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/HandleJSONP")
public class HandleJSONP extends HttpServlet {
private static final long serialVersionUID = 1L;
public HandleJSONP() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String callbackName = request.getParameter("callbackName");
String xm = request.getParameter("xm");
String age = request.getParameter("age");
String idno = request.getParameter("idno");
System.out.println(xm);
System.out.println(age);
System.out.println(idno);
response.getWriter().write(callbackName + "('Data Sent From Server!')");
}
}
原理展示:
3. postMessage 跨域
3.1. 原理
window.postMessage() 方法可以安全地实现跨源通信,可用于:
API:
targetWindow.postMessage(message, targetOrigin);
兼容性:
3.2. 示例
postmessage-father.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>PostMessage-Father</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<div>
<iframe src="http://192.168.56.1:8180/cross-orgin/postmessage-child.jsp"
style="width: 600px; height: 200px;"></iframe>
</div>
<script>
window.addEventListener('message', function(e) {
var origin = e.origin;
var data = e.data;
var source = e.source;
console.log(origin, data);
alert("Received message: "+data);
}, false);
</script>
</body>
</html>
postmessage-child.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>PostMessage-Child</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<button onclick="postToFather()">postMessage to father window!</button>
<script>
function postToFather(){
var data = {
name: '张三',
age: '18'
};
var targetWindow = window.parent;
targetWindow.postMessage(
JSON.stringify(data),
'http://localhost:8080'
);
}
</script>
</body>
</html>
4. iframe 跨域
4.1. iframe + iframe
核心原理
A域 -> B域 -> A域 ==> parent.parent
parent.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript">
function fnParent(){
alert('Parent function executed!');
}
function callChild(){
var iframe = document.getElementById("execIframe");
iframe.src = "http://127.0.0.1:8181/crossdomain/iframe-crossorigin/childCaller.jsp?_=" + Math.random();
}
</script>
<h2>Parent(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h2>
<p>
<button onclick="callChild()">Call Child</button>
</p>
<iframe src="http://127.0.0.1:8181/crossdomain/iframe-crossorigin/child.jsp"
id="childFrame" name="childFrame"
width="500" height="200"></iframe>
<iframe id="execIframe" src="" width="500" height="70"></iframe>
</body></html>
child.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript">
function callParent(){
var iframe = document.getElementById("execIframe");
iframe.src = "http://192.168.124.10:8080/crossdomain/iframe-crossorigin/parentCaller.jsp?_=" + Math.random();
}
function fnChild(){
alert('Child function executed!');
}
</script>
<h2>Child(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h2>
<button onclick="callParent()">Call Parent</button>
<iframe id="execIframe" src="" width="400" height="70"></iframe>
</body></html>
childCaller.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript">
parent.window.childFrame.fnChild();
</script>
<h3>Child Caller(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h3>
</body></html>
parentCaller.jsp:
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
<script type="text/javascript">
parent.parent.fnParent();
</script>
<h3>Parent Caller(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h3>
</body></html>
效果展示:
原理分析:父->子
原理分析:子->父
4.2. iframe + location.hash
URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变 URL 的 hash 部分来进行双向通信。
hash-parent.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hash-Parent</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<button onclick="sendMsgToChild()">send message to child!</button>
<div>
<iframe id="iframe"
src="http://192.168.56.1:8180/cross-orgin/hash-child.jsp"
style="width: 700px; height: 400px;"></iframe>
</div>
<script>
function sendMsgToChild(){
var iframe = document.getElementById('iframe');
iframe.src = iframe.src + '#a_message_to_child';
}
function onMsgReceived(res) {
alert('Parent received: ' + res);
}
</script>
</body>
</html>
hash-child.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hash-Child</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<div>
<iframe id="iframe"
src="http://localhost:8080/cross-orgin/hash-proxy.jsp"
style="width: 600px; height: 200px;"></iframe>
</div>
<script>
var iframe = document.getElementById('iframe');
// 监听 hash-parent.jsp 传来的 hash 值,再传给 hash-proxy.html
window.onhashchange = function () {
var hash = location.hash;
alert("Child received: "+hash);
iframe.src = iframe.src + "#a_message_from_child";
};
</script>
</body>
</html>
hash-proxy.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hash-Proxy</h1>
<h3 id="address"></h3>
<script>
window.onload = function(){
document.getElementById("address").innerText = location.href;
}
</script>
<script>
// 监听hash-child.html传来的hash值
window.onhashchange = function () {
var hash = location.hash;
// 再通过操作同域hash-parent.jsp的js回调,将结果传回
window.parent.parent.onMsgReceived(hash);
};
</script>
</body>
</html>
5. CORS 跨域
5.1. 定义
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own.
5.2. 两种请求
5.3. 简单请求
5.3.1 什么情况下是简单请求?
满足下面所有条件的请求,就是“简单请求”:
5.3.2 基本流程?
5.4. 预检请求
5.4.1 为什么要预检?基本流程是什么?
5.4.2 基本流程?
5.4.3 预检请求缓存
Access-Control-Max-Age gives the value in seconds for how long the response to the preflight request can be cached for without sending another preflight request.
Access-Control-Max-Age: <delta-seconds>
5.5. 身份凭证问题(Credentials)
Access-Control-Allow-Credentials: true
5.6. 响应头获取问题(response->headers)
在跨域访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果要访问其他头(例如:Content-Disposition),则需要在服务器端设置 Access-Control-Expose-Headers 响应头,允许浏览器访问其他响应头。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
5.7. CORS 相关 HTTP 头部字段
5.7.1 请求头部字段
Origin: <origin>
Access-Control-Request-Method: <method>
Access-Control-Request-Headers: <field-name>[, <field-name>]*
5.7.2 响应头部字段
Access-Control-Allow-Origin: <origin> | *
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age: <delta-seconds>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: <method>[, <method>]* // 用于预检请求的响应
Access-Control-Allow-Headers: <field-name>[, <field-name>]* // 用于预检请求的响应
6. Nginx 跨域
7. Websocket 跨域
WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。
WebSocket 支持跨域通信
参考:
The Web Origin Concept: https://tools.ietf.org/html/rfc6454 Same-origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy Same-origin policy: https://web.dev/same-origin-policy/ Window.postMessage(): https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage WebSockets: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API HTTP访问控制(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS Cross-Origin Resource Sharing: https://www.w3.org/TR/cors/ XDomainRequest - Restrictions, Limitations and Workarounds: https://docs.microsoft.com/en-us/archive/blogs/ieinternals/xdomainrequest-restrictions-limitations-and-workarounds