瀏覽器跨域問題的總結
目錄
JavaScript出於安全方面的考慮,不允許跨域調用其他頁面的對象。但在安全限制的同時也給注入iframe或是ajax應用上帶來了不少麻煩。我們在項目開發中遇到了幾種跨域情形,現在進行一些總結,也對互聯網上凌亂的東西進行一些總結,着重描述上述一些情景,以下列舉跨域問題的幾種情形
URL |
說明 |
是否允許通信 |
http://www.a.com/a.js |
同一域名下 |
允許 |
http://www.a.com/lab/a.js |
同一域名下不同文件夾 |
允許 |
http://www.a.com:8000/a.js |
同一域名,不同端口 |
不允許 |
http://www.a.com/a.js |
同一域名,不同協議 |
不允許 |
http://www.a.com/a.js |
域名和域名對應ip |
不允許 |
http://www.a.com/a.js |
主域相同,子域不同 |
不允許 |
http://www.a.com/a.js |
同一域名,不同二級域名(同上) |
不允許(cookie這種情況下也不允許訪問) |
http://www.cnblogs.com/a.js |
不同域名 |
不允許 |
特別注意兩點:
第一,如果是協議和端口造成的跨域問題“前臺”是無能爲力的,
第二:在跨域問題上,域僅僅是通過“URL的首部”來識別而不會去嘗試判斷相同的ip地址對應着兩個域或兩個域是否在同一個ip上。
“URL的首部”指window.location.protocol +window.location.host,也可以理解爲“Domains, protocols and ports must match”。
對於主域相同而子域不同的例子,可以通過設置document.domain的辦法來解決。具體的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html兩個文件中分別加上document.domain = ‘a.com’;然後通過a.html文件中創建一個iframe,去控制iframe的contentDocument,這樣兩個js文件之間就可以“交互”了。當然這種辦法只能解決主域相同而二級域名不同的情況,如果你異想天開的把script.a.com的domian設爲alibaba.com那顯然是會報錯地!代碼如下:
www.a.com上的a.html
document.domain = 'a.com';
var ifr = document.createElement_x('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在這裏操縱b.html
alert(doc.getElementsByTagName_r("h1")[0].childNodes[0].nodeValue);
};
script.a.com上的b.html
document.domain = 'a.com';
這種方式適用於{www.kuqin.com, kuqin.com, script.kuqin.com, css.kuqin.com}中的任何頁面相互通信。
備註:
某一頁面的domain默認等於window.location.hostname。主域名是不帶www的域名,例如a.com,主域名前面帶前綴的通常都爲二級域名或多級域名,例如www.a.com其實是二級域名。 domain只能設置爲主域名,不可以在b.a.com中將domain設置爲c.a.com。
問題:
1、安全性,當一個站點(b.a.com)被攻擊後,另一個站點(c.a.com)會引起安全漏洞。
2、如果一個頁面中引入多個iframe,要想能夠操作所有iframe,必須都得設置相同domain。我們工行商城IM聯調過程中嘗試過這種方式,最終因爲影響頁面其他iframe的功能而放棄。最終我們調整了融e購的IM前端結構.
JSONP是一個非官方的協議,它允許在服務器端集成Script tags返回至客戶端,通過javascript callback的形式實現跨域訪問(這僅僅是JSONP簡單的實現形式)。JSON系統開發方法是一種典型的面向數據結構的分析和設計方法,以活動爲中心,一連串的活動的順序組合成一個完整的工作進程。
$.getJSON(url+"?callback=?",
function(json){
});
$.ajax({
url: '', // 跨域URL
type: 'GET',
dataType: 'jsonp',
jsonp: 'jsoncallback', //默認callback
data: mydata, //請求數據
timeout: 5000,
success: function (json) { //客戶端jQuery預先定義好的callback函數,成功獲取跨域服務器上的json數據後,會動態執行這個callback函數
if(json.actionErrors.length!=0){
alert(json.actionErrors);
}
},
complete: function(XMLHttpRequest, textStatus){
}
});
3
.服務器端的跨域解決方案
最新的W3C標準裏是這麼實現HTTP跨域請求的Cross-Origin Resource Sharing跨域的目標服務器要返回一系列的Headers,通過這些Headers來控制是否同意跨域。
Access-Control-Allow-Origin 這個 Header在W3C標準裏用來檢查該跨域請求是否可以被通過。
從 http://www.a.com/test.html 發起一個跨域請求,請求的地址爲: http://www.b.com/test.php 如果 服務器B返回一個如下的header,Access-Control-Allow-Origin: http://www.a.com,那麼,這個來自 http://www.a.com/test.html 的跨域請求就會被通過。在這個過程中, request 還會帶上這個header,Access-Control-Allow-Origin 的值可以是通配符 *
如果是 * 的話,就可以接收來自任意source origin的請求。
IE8 問題(詳情見情景4), 則是通過 XDomainRequest 來實現的這個跨域請求比如類似如下代碼就可以實現了:
var request = new XDomainRequest();
request.open("GET", xdomainurl);
request.send();
也要求對方服務器返回這個頭才行。
服務器端代碼的操作示例:
1.我們可以在Java代碼中加入 response.setHeader("Access-Control-Allow-Origin", "*");
2.html的,須要
3.“CrossOriginFilter”類,用於支持跨域的 JavaScript 請求,如果您的項目中要支持跨域的服務器推送,可以加入該配置。
package net.icbc.messager.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.icbc.messager.web.servlet.TokenServlet;
import org.apache.log4j.Logger;
public class CrossOriginFilter implements Filter
{
private static Logger log = Logger.getLogger(TokenServlet.class);
// Request headers
private static final String ORIGIN_HEADER = "Origin";
private static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
private static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
// Response headers
private static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
private static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
private static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
private static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
private static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
// Implementation constants
private static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
private static final String ALLOWED_METHODS_PARAM = "allowedMethods";
private static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
private static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
private static final String ALLOWED_CREDENTIALS_PARAM = "allowCredentials";
private static final String ANY_ORIGIN = "*";
private static final List SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
private boolean anyOriginAllowed = false;
private List allowedOrigins = new ArrayList();
private List allowedMethods = new ArrayList();
private List allowedHeaders = new ArrayList();
private int preflightMaxAge = 0;
private boolean allowCredentials = false;
public void init(FilterConfig config) throws ServletException
{
String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
if (allowedOriginsConfig == null) allowedOriginsConfig = "*";
String[] allowedOrigins = allowedOriginsConfig.split(",");
for (String allowedOrigin : allowedOrigins)
{
allowedOrigin = allowedOrigin.trim();
if (allowedOrigin.length() > 0)
{
if (ANY_ORIGIN.equals(allowedOrigin))
{
anyOriginAllowed = true;
this.allowedOrigins.clear();
break;
}
else
{
this.allowedOrigins.add(allowedOrigin);
}
}
}
String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
if (allowedMethodsConfig == null) allowedMethodsConfig = "GET,POST";
allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
if (allowedHeadersConfig == null) allowedHeadersConfig = "X-Requested-With,Content-Type,Accept,Origin";
allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(",")));
String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
if (preflightMaxAgeConfig == null) preflightMaxAgeConfig = "1800"; // Default is 30 minutes
try
{
preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
}
catch (NumberFormatException x)
{
log.info("Cross-origin filter, could not parse '{}' parameter as integer: {}"+ PREFLIGHT_MAX_AGE_PARAM+ preflightMaxAgeConfig);
}
String allowedCredentialsConfig = config.getInitParameter(ALLOWED_CREDENTIALS_PARAM);
if (allowedCredentialsConfig == null) allowedCredentialsConfig = "false";
allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
log.debug("Cross-origin filter configuration: " +
ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
ALLOWED_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig);
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
{
String origin = request.getHeader(ORIGIN_HEADER);
// Is it a cross origin request ?
if (origin != null && isEnabled(request))
{
if (originMatches(origin))
{
if (isSimpleRequest(request))
{
log.debug("Cross-origin request to {} is a simple cross-origin request"+ request.getRequestURI());
handleSimpleResponse(request, response, origin);
}
else
{
log.debug("Cross-origin request to {} is a preflight cross-origin request" +request.getRequestURI());
handlePreflightResponse(request, response, origin);
}
}
else
{
log.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
}
}
chain.doFilter(request, response);
}
protected boolean isEnabled(HttpServletRequest request)
{
// WebSocket clients such as Chrome 5 implement a version of the WebSocket
// protocol that does not accept extra response headers on the upgrade response
if ("Upgrade".equalsIgnoreCase(request.getHeader("Connection")) &&
"WebSocket".equalsIgnoreCase(request.getHeader("Upgrade")))
{
return false;
}
return true;
}
private boolean originMatches(String origin)
{
if (anyOriginAllowed) return true;
for (String allowedOrigin : allowedOrigins)
{
if (allowedOrigin.equals(origin)) return true;
}
return false;
}
private boolean isSimpleRequest(HttpServletRequest request)
{
String method = request.getMethod();
if (SIMPLE_HTTP_METHODS.contains(method))
{
// TODO: implement better section 6.1
// Section 6.1 says that for a request to be simple, custom request headers must be simple.
// Here for simplicity I just check if there is a Access-Control-Request-Method header,
// which is required for preflight requests
return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
}
return false;
}
private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
{
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
}
private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
{
// Implementation of section 5.2
// 5.2.3 and 5.2.5
boolean methodAllowed = isMethodAllowed(request);
if (!methodAllowed) return;
// 5.2.4 and 5.2.6
boolean headersAllowed = areHeadersAllowed(request);
if (!headersAllowed) return;
// 5.2.7
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
// 5.2.8
if (preflightMaxAge > 0) response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
// 5.2.9
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
// 5.2.10
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
}
private boolean isMethodAllowed(HttpServletRequest request)
{
String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
log.debug("{} is {}"+ ACCESS_CONTROL_REQUEST_METHOD_HEADER+ accessControlRequestMethod);
boolean result = false;
if (accessControlRequestMethod != null)
{
result = allowedMethods.contains(accessControlRequestMethod);
}
log.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}"+accessControlRequestMethod+allowedMethods);
return result;
}
private boolean areHeadersAllowed(HttpServletRequest request)
{
String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
log.debug("{} is {}"+ ACCESS_CONTROL_REQUEST_HEADERS_HEADER+ accessControlRequestHeaders);
boolean result = true;
if (accessControlRequestHeaders != null)
{
String[] headers = accessControlRequestHeaders.split(",");
for (String header : headers)
{
boolean headerAllowed = false;
for (String allowedHeader : allowedHeaders)
{
if (header.trim().equalsIgnoreCase(allowedHeader.trim()))
{
headerAllowed = true;
break;
}
}
if (!headerAllowed)
{
result = false;
break;
}
}
}
log.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}"+accessControlRequestHeaders+allowedHeaders);
return result;
}
private String commify(List strings)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strings.size(); ++i)
{
if (i > 0) builder.append(",");
String string = strings.get(i);
builder.append(string);
}
return builder.toString();
}
public void destroy()
{
anyOriginAllowed = false;
allowedOrigins.clear();
allowedMethods.clear();
allowedHeaders.clear();
preflightMaxAge = 0;
allowCredentials = false;
}
}
Fileter配置信息:
cross-origin
net.icbc.messager.web.CrossOriginFilter
cross-origin
/*
4.IE8下的跨域問題
情景4需在情景3的基礎上進行,僅僅進行情景3的設置,是在IE8下行不通的.
通過使用 Internet Explorer 8 中的跨域請求(縮寫爲“XDR”),開發人員可以創建跨網站數據聚合方案。 這個名爲 XDomainRequest 的請求與 XMLHttpRequest 對象類似,但編程模型更加簡單,它可以提供一種最簡單的方式來向支持 XDR 的第三方站點發出匿名請求,並選擇使這些站點的數據可跨域使用。 只需三行代碼即可生成基本的跨站點請求。 這將確保針對公共站點(例如,博客或其他社交網絡應用程序)的數據聚合簡單、安全和快速。
下面的 JavaScript 代碼介紹 XDomainRequest 對象及其事件、屬性和方法。 XDomainRequest 參考頁提供了比此處更爲詳細的信息。
// Creates a new XDR object.
xdr = new XDomainRequest();
// Indicates there is an error and the request cannot be completed.
xdr.onerror = alert_error;
// The request has reached its timeout.
xdr.ontimeout = alert_timeout;
// The object has started returning data.
xdr.onprogress = alert_progress;
// The object is complete.
xdr.onload = alert_loaded;
// Sets the timeout interval.
xdr.timeout = timeout;
// Gets the content-type header in the request.
var content_type = xdr.contentType;
// Gets the body of the response.
var response = xdr.responseText;
// Creates a connection with a domain's server.
xdr.open("get", url);
// Transmits a data string to the server.
xdr.send();
// Terminates a pending send.
xdr.abort();
附:IE8下的跨域js的兼容設置,實現XDR對象到XHR對象的轉換 ieXDRToXHR.js (注此js會對jquery形成干擾)
if (window.XDomainRequest) {
windows.ieXDRToXHR = function(window) {
"use strict";
var XHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
this.onreadystatechange = Object;
this.xhr = null;
this.xdr = null;
this.readyState = 0;
this.status = '';
this.statusText = null;
this.responseText = null;
this.getResponseHeader = null;
this.getAllResponseHeaders = null;
this.setRequestHeader = null;
this.abort = null;
this.send = null;
this.isxdr = false;
// static binding
var self = this;
self.xdrLoadedBinded = function() {
self.xdrLoaded();
};
self.xdrErrorBinded = function() {
self.xdrError();
};
self.xdrProgressBinded = function() {
self.xdrProgress();
};
self.xhrReadyStateChangedBinded = function() {
self.xhrReadyStateChanged();
};
};
XMLHttpRequest.prototype.open = function(method, url, asynch, user, pwd) {
//improve CORS deteciton (chat.example.NET exemple.net), remove hardcoded http-bind
var parser = document.createElement_x('a');
parser.href = url;
if (parser.hostname!=document.domain) {
if (this.xdr === null){
this.xdr = new window.XDomainRequest();
}
this.isxdr = true;
this.setXDRActive();
this.xdr.open(method, url);
} else {
if (this.xhr === null){
this.xhr = new XHR();
}
this.isxdr = false;
this.setXHRActive();
this.xhr.open(method, url, asynch, user, pwd);
}
};
XMLHttpRequest.prototype.xdrGetResponseHeader = function(name) {
if (name === 'Content-Type' && this.xdr.contentType > ''){
return this.xdr.contentType;
}
return '';
};
XMLHttpRequest.prototype.xdrGetAllResponseHeaders = function() {
return (this.xdr.contentType > '') ? 'Content-Type: ' + this.xdr.contentType : '';
};
XMLHttpRequest.prototype.xdrSetRequestHeader = function(name, value) {
//throw new Error('Request headers not supported');
};
XMLHttpRequest.prototype.xdrLoaded = function() {
if (this.onreadystatechange !== null) {
this.readyState = 4;
this.status = 200;
this.statusText = 'OK';
this.responseText = this.xdr.responseText;
if (window.ActiveXObject){
var doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async='false';
doc.loadXML(this.responseText);
this.responseXML = doc;
}
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.xdrError = function() {
if (this.onreadystatechange !== null) {
this.readyState = 4;
this.status = 0;
this.statusText = '';
// ???
this.responseText = '';
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.xdrProgress = function() {
if (this.onreadystatechange !== null && this.status !== 3) {
this.readyState = 3;
this.status = 3;
this.statusText = '';
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.finalXDRRequest = function() {
var xdr = this.xdr;
delete xdr.onload;AZ
delete xdr.onerror;
delete xdr.onprogress;
};
XMLHttpRequest.prototype.sendXDR = function(data) {
var xdr = this.xdr;
xdr.onload = this.xdrLoadedBinded;
xdr.onerror = this.xdr.ontimeout = this.xdrErrorBinded;
xdr.onprogress = this.xdrProgressBinded;
this.responseText = null;
this.xdr.send(data);
};
XMLHttpRequest.prototype.abortXDR = function() {
this.finalXDRRequest();
this.xdr.abort();
};
XMLHttpRequest.prototype.setXDRActive = function() {
this.send = this.sendXDR;
this.abort = this.abortXDR;
this.getResponseHeader = this.xdrGetResponseHeader;
this.getAllResponseHeaders = this.xdrGetAllResponseHeaders;
this.setRequestHeader = this.xdrSetRequestHeader;
};
XMLHttpRequest.prototype.xhrGetResponseHeader = function(name) {
return this.xhr.getResponseHeader(name);
};
XMLHttpRequest.prototype.xhrGetAllResponseHeaders = function() {
return this.xhr.getAllResponseHeaders();
};
XMLHttpRequest.prototype.xhrSetRequestHeader = function(name, value) {
return this.xhr.setRequestHeader(name, value);
};
XMLHttpRequest.prototype.xhrReadyStateChanged = function() {
if (this.onreadystatechange !== null && this.readyState !== this.xhr.readyState) {
var xhr = this.xhr;
this.readyState = xhr.readyState;
if (this.readyState === 4) {
this.status = xhr.status;
this.statusText = xhr.statusText;
this.responseText = xhr.responseText;
this.responseXML = xhr.responseXML;
}
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.finalXHRRequest = function() {
delete this.xhr.onreadystatechange;
};
XMLHttpRequest.prototype.abortXHR = function() {
this.finalXHRRequest();
this.xhr.abort();
};
XMLHttpRequest.prototype.sendXHR = function(data) {
this.xhr.onreadystatechange = this.xhrReadyStateChangedBinded;
this.xhr.send(data);
};
XMLHttpRequest.prototype.setXHRActive = function() {
this.send = this.sendXHR;
this.abort = this.abortXHR;
this.getResponseHeader = this.xhrGetResponseHeader;
this.getAllResponseHeaders = this.xhrGetAllResponseHeaders;
this.setRequestHeader = this.xhrSetRequestHeader;
};
windows.ieXDRToXHR = undefined;
};
windows.ieXDRToXHR(window);
}