CORS跨域實現思路及相關解決方案
本篇包括以下內容:
- CORS 定義
- CORS 對比 JSONP
- CORS,BROWSER支持情況
- 主要用途
- Ajax請求跨域資源的異常
- CORS 實現思路
- 安全說明
- CORS 幾種解決方案
- 自定義CORSFilter
- Nginx 配置支持Ajax跨域
- 支持多域名配置的CORS Filter
keyword:cors,跨域,ajax,403,filter,RESTful,origin,http,nginx,jsonp
轉載自:http://www.cnblogs.com/sloong/p/cors.html
CORS 定義
Cross-Origin Resource Sharing(CORS)跨來源資源共享是一份瀏覽器技術的規範,提供了 Web 服務從不同域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,是 JSONP 模式的現代版。與 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。用 CORS 可以讓網頁設計師用一般的 XMLHttpRequest,這種方式的錯誤處理比 JSONP 要來的好。另一方面,JSONP 可以在不支持 CORS 的老舊瀏覽器上運作。現代的瀏覽器都支持 CORS。
CORS是W3c工作草案,它定義了在跨域訪問資源時瀏覽器和服務器之間如何通信。CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和服務器相互了解對方,從而決定請求或響應成功與否。W3C CORS 工作草案
同源策略:是瀏覽器最核心也最基本的安全功能;同源指的是:同協議,同域名和同端口。精髓:認為自任何站點裝載的信賴內容是不安全的。當被瀏覽器半信半疑的腳本運行在沙箱時,它們應該只被允許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源;參考:JavaScript 的同源策略
JSON & JSONP:JSON 是一種基於文本的數據交換方式,或者叫做數據描述格式。JSONP是資料格式JSON的一種“使用模式”,可以讓網頁從別的網域要資料,由於同源策略,一般來說位於server1.example.com的網頁無法與不是 server1.example.com的服務器溝通,而HTML的script元素是一個例外。利用script元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON資料,而這種使用模式就是所謂的JSONP
CORS 對比 JSONP
都能解決 Ajax直接請求普通文件存在跨域無權限訪問的問題
- JSONP只能實現GET請求,而CORS支持所有類型的HTTP請求
- 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理
- JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數現代瀏覽器都已經支持了CORS
CORS,BROWSER支持情況
數據來源:caniuse.com
IE6,IE7,Opera min 不支持CORS。具體可參看數據來源中的 ‘show all‘
主要用途
- From a browser script perspective
- From a web service perspective: By utilising the origin URL reported by the browser the target cross-domain web service can determine, based on its origin policy, whether to allow or deny the request.
Ajax請求跨域資源的異常
當出現如下異常時,那麽就需要考慮跨域的問題了
例如 localhost:63343 通過Ajax請求http://192.168.10.61:8080服務器資源時就會出現如下異常:
CORS 實現思路
CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和服務器相互了解對方,從而決定請求或響應成功與否
安全說明
CORS is not about providing server-side security. The Origin request header is produced by the browser and the server has no direct means to verify it.
CORS 並不是為了解決服務端安全問題,而是為了解決如何跨域調用資源。至於如何設計出 安全的開放API,卻是另一個問題了,這裏提下一些思路:
- 請求時間有效性(驗證timestamp與服務接到請求的時間相差是否在指定範圍內,比如5分鐘內)
- token驗證
- ip驗證
- 來源驗證
例如
{
‘name‘: 用戶名,
‘key: 加密的驗證key,//(name+secret+timestamp來通過不可逆加密生成)
‘timestamp’: 時間戳,//驗證timestamp與服務接到請求的時間相差是否在指定範圍內,比如5分鐘內
}
CORS 幾種解決方案
CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和服務器相互了解對方,從而決定請求或響應成功與否.
Access-Control-Allow-Origin:指定授權訪問的域
Access-Control-Allow-Methods:授權請求的方法(GET, POST, PUT, DELETE,OPTIONS等)
一:簡單的自定義CORSFilter / Interceptor
適合設置單一的(或全部)授權訪問域,所有配置都是固定的,特簡單。也沒根據請求的類型做不同的處理
在web.xml 中添加filter
<filter> <filter-name>cros</filter-name> <filter-class>cn.ifengkou.test.filter.CORSFilter</filter-class> </filter> <filter-mapping> <filter-name>cros</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
新增CORSFilter 類
@Component
public class CORSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
response.addHeader("Access-Control-Allow-Headers", "Content-Type");
response.addHeader("Access-Control-Max-Age", "1800");//30 min
filterChain.doFilter(request, response);
}
}
Access-Control-Allow-Origin只能配置 或者一個域名*
比如配置了192.168.56.130,那麽只有192.168.56.130 能拿到數據,否則全部報403異常
response.addHeader("Access-Control-Allow-Origin", "http://192.168.56.130");
二:Nginx 配置支持Ajax跨域
這裏是一個nginx啟用COSR的參考配置:來源
#
# Wide-open CORS config for nginx
#
location / {
if ($request_method = ‘OPTIONS‘) {
add_header ‘Access-Control-Allow-Origin‘ ‘*‘;
add_header ‘Access-Control-Allow-Methods‘ ‘GET, POST, OPTIONS‘;
#
# Custom headers and headers various browsers *should* be OK with but aren‘t
#
add_header ‘Access-Control-Allow-Headers‘ ‘DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type‘;
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header ‘Access-Control-Max-Age‘ 1728000;
add_header ‘Content-Type‘ ‘text/plain charset=UTF-8‘;
add_header ‘Content-Length‘ 0;
return 204;
}
if ($request_method = ‘POST‘) {
add_header ‘Access-Control-Allow-Origin‘ ‘*‘;
add_header ‘Access-Control-Allow-Methods‘ ‘GET, POST, OPTIONS‘;
add_header ‘Access-Control-Allow-Headers‘ ‘DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type‘;
}
if ($request_method = ‘GET‘) {
add_header ‘Access-Control-Allow-Origin‘ ‘*‘;
add_header ‘Access-Control-Allow-Methods‘ ‘GET, POST, OPTIONS‘;
add_header ‘Access-Control-Allow-Headers‘ ‘DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type‘;
}
}
三:支持多域名配置的CORS Filter
因為知道已經有可以用的庫可以解決,所以就沒重復造輪子了。其實因為懶,看看別人的源碼算了。。。
在mvnrepository搜索cors-filter,目前也就兩個可以用
- org.ebaysf.web 的 cors-filter,項目地址:https://github.com/ebay/cors-filter
- com.thetransactioncompany的 cors-filter,項目地址:http://software.dzhuvinov.com/cors-filter.html
這兩個也都大同小異,因為ebay開源在github上,也有詳細的README,那麽就以ebay的cors-filter為例
配置
添加依賴包到項目:
<dependency> <groupId>org.ebaysf.web</groupId> <artifactId>cors-filter</artifactId> <version>1.0.1</version> </dependency>
添加配置(具體配置項,還是見項目的README.md吧)
<filter> <filter-name>CORS Filter</filter-name> <filter-class>org.ebaysf.web.cors.CORSFilter</filter-class> <init-param> <param-name>cors.allowed.origins</param-name> <param-value>http://192.168.56.129,http://192.168.56.130</param-value> </init-param> <init-param> <param-name>cors.allowed.methods</param-name> <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value> </init-param> <init-param> <param-name>cors.allowed.headers</param-name> <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value> </init-param> </filter> <filter-mapping> <filter-name>CORS Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
源碼分析
源碼地址:github。但通過IDEA Decompiled 出來的更清晰.....,以下是反編譯的
ebaysf的cors-filter 只有一個類CORSFilter。也就是一個攔截器,implements Filter
public final class CORSFilter implements Filter {
通過是實現Filter 的init 方法從配置文件中讀取參數:
public void init(FilterConfig filterConfig) throws ServletException {
this.parseAndStore("*", "GET,POST,HEAD,OPTIONS", "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers", "", "true", "1800", "false", "true");
this.filterConfig = filterConfig;
this.loggingEnabled = false;
if(filterConfig != null) {
String configAllowedOrigins = filterConfig.getInitParameter("cors.allowed.origins");
String configAllowedHttpMethods = filterConfig.getInitParameter("cors.allowed.methods");
String configAllowedHttpHeaders = filterConfig.getInitParameter("cors.allowed.headers");
String configExposedHeaders = filterConfig.getInitParameter("cors.exposed.headers");
String configSupportsCredentials = filterConfig.getInitParameter("cors.support.credentials");
String configPreflightMaxAge = filterConfig.getInitParameter("cors.preflight.maxage");
String configLoggingEnabled = filterConfig.getInitParameter("cors.logging.enabled");
String configDecorateRequest = filterConfig.getInitParameter("cors.request.decorate");
this.parseAndStore(configAllowedOrigins, configAllowedHttpMethods, configAllowedHttpHeaders, configExposedHeaders, configSupportsCredentials, configPreflightMaxAge, configLoggingEnabled, configDecorateRequest);
}
}
parseAndStore 方法,解析參數。以 解析cors.allowed.orgins為例;其他參數同理
Set e; if(allowedOrigins != null) { if(allowedOrigins.trim().equals("*")) { this.anyOriginAllowed = true; } else { this.anyOriginAllowed = false; e = this.parseStringToSet(allowedOrigins); this.allowedOrigins.clear(); this.allowedOrigins.addAll(e); } } //parseStringToSet //對多域名用點分割,加到HashSet中,再賦給allowedOrigins(Collection<String> allowedOrigins = new HashSet();) private Set<String> parseStringToSet(String data) { String[] splits; if(data != null && data.length() > 0) { splits = data.split(","); } else { splits = new String[0]; } HashSet set = new HashSet(); if(splits.length > 0) { String[] arr$ = splits; int len$ = splits.length; for(int i$ = 0; i$ < len$; ++i$) { String split = arr$[i$]; set.add(split.trim()); } } return set; }
如何實現 doFilter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
HttpServletRequest request1 = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//識別request 屬於哪種類別
CORSFilter.CORSRequestType requestType = this.checkRequestType(request1);
if(this.decorateRequest) {
decorateCORSProperties(request1, requestType);
}
switch(CORSFilter.SyntheticClass_1.$SwitchMap$org$ebaysf$web$cors$CORSFilter$CORSRequestType[requestType.ordinal()]) {
case 1:
this.handleSimpleCORS(request1, response, filterChain);
break;
case 2:
this.handleSimpleCORS(request1, response, filterChain);
break;
case 3:
this.handlePreflightCORS(request1, response, filterChain);
break;
case 4:
this.handleNonCORS(request1, response, filterChain);
break;
default:
this.handleInvalidCORS(request1, response, filterChain);
}
} else {
String request = "CORS doesn\‘t support non-HTTP request or response.";
throw new ServletException(request);
}
}
判斷request類別,根據類別進行差異化處理。handleSimpleCORS 處理過程,判斷是否設置允許所有origin參數,判斷是否符合httpMethods要求,判斷此次request的origin(origin = request.getHeader("Origin"))是否在allowedOrigins(origin白名單)內。如果在,就設置response.addHeader("Access-Control-Allow-Origin", origin);這樣也就實現了多域名支持。流程圖就不畫了...
public void handleSimpleCORS(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
CORSFilter.CORSRequestType requestType = this.checkRequestType(request);
String origin;
if(requestType != CORSFilter.CORSRequestType.SIMPLE && requestType != CORSFilter.CORSRequestType.ACTUAL) {
origin = "Expects a HttpServletRequest object of type " + CORSFilter.CORSRequestType.SIMPLE + " or " + CORSFilter.CORSRequestType.ACTUAL;
throw new IllegalArgumentException(origin);
} else {
origin = request.getHeader("Origin");
String method = request.getMethod();
if(!this.isOriginAllowed(origin)) {
this.handleInvalidCORS(request, response, filterChain);
} else if(!this.allowedHttpMethods.contains(method)) {
this.handleInvalidCORS(request, response, filterChain);
} else {
if(this.anyOriginAllowed && !this.supportsCredentials) {
response.addHeader("Access-Control-Allow-Origin", "*");
} else {
response.addHeader("Access-Control-Allow-Origin", origin);
}
if(this.supportsCredentials) {
response.addHeader("Access-Control-Allow-Credentials", "true");
}
if(this.exposedHeaders != null && this.exposedHeaders.size() > 0) {
String exposedHeadersString = join(this.exposedHeaders, ",");
response.addHeader("Access-Control-Expose-Headers", exposedHeadersString);
}
filterChain.doFilter(request, response);
}
}
}
為了避免對參數一知半解,就把作者的參數描述表貼上來,通過參數表可以了解下header裏面各個參數的作用
param-name | description |
---|---|
cors.allowed.origins | A list of origins that are allowed to access the resource. A ‘‘ can be specified to enable access to resource from any origin. Otherwise, a whitelist of comma separated origins can be provided. Ex: http://www.w3.org, https://www.apache.org. Defaults: (Any origin is allowed to access the resource). |
cors.allowed.methods | A comma separated list of HTTP methods that can be used to access the resource, using cross-origin requests. These are the methods which will also be included as part of ‘Access-Control-Allow-Methods‘ header in a pre-flight response. Ex: GET,POST. Defaults:GET,POST,HEAD,OPTIONS |
cors.allowed.headers | A comma separated list of request headers that can be used when making an actual request. These header will also be returned as part of ‘Access-Control-Allow-Headers‘ header in a pre-flight response. Ex: Origin,Accept. Defaults: Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers |
cors.exposed.headers | A comma separated list of headers other than the simple response headers that browsers are allowed to access. These are the headers which will also be included as part of ‘Access-Control-Expose-Headers‘ header in the pre-flight response. Ex: X-CUSTOM-HEADER-PING,X-CUSTOM-HEADER-PONG. Default: None |
cors.preflight.maxage | The amount of seconds, browser is allowed to cache the result of the pre-flight request. This will be included as part of ‘Access-Control-Max-Age‘ header in the pre-flight response. A negative value will prevent CORS Filter from adding this response header from pre-flight response. Defaults: 1800 |
cors.support.credentials | A flag that indicates whether the resource supports user credentials. This flag is exposed as part of ‘Access-Control-Allow-Credentials‘ header in a pre-flight response. It helps browser determine whether or not an actual request can be made using credentials. Defaults:true |
cors.logging.enabled | A flag to control logging to container logs. Defaults: false |
cors.request.decorate | A flag to control if the request should be decorated or not. Defaults: true |
測試:
1.服務端準備接口(我的地址是:http://192.168.10.61:8080/api)
@RequestMapping(method = RequestMethod.GET,value = "test") @ResponseBody public HashMap<String,Object> getArticles(){ HashMap<String,Object> map = new HashMap<>(); map.put("result","success"); return map; }
2.過濾器配置(web.xml),配置允許訪問的域為:http://192.168.56.129,http://www.website2.com
<filter> <filter-name>CORS Filter</filter-name> <filter-class>org.ebaysf.web.cors.CORSFilter</filter-class> <init-param> <param-name>cors.allowed.origins</param-name> <param-value>http://192.168.56.129,http://www.website2.com</param-value> </init-param> </filter> <filter-mapping> <filter-name>CORS Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
3.準備測試網頁index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>cors test page</title> <script src="jquery.min.js"></script> <script> function loadData(){ $.ajax({ url: "http://192.168.10.61:8080/api", type:"GET", dataType:"json", timeout:10000, success:function(data){ $("#result").append(data.result+"<br />"); console.log(data); }, error:function(e){ $("#result").append(e.statusText+"<br />"); } }); } $(function(){ $("#host").append("origin:"+window.location.origin); }); </script> </head> <body> <button onclick="loadData()">onclick</button> <div id="host"></div> <div id="result" style="height:200px;width:100%"></div> </body> </html>
4.將index.html發布到nginx(nginx後面也有方案)
index.html 不能直接用瀏覽器打開運行,雖然可以調用Ajax請求,但是域是file:///path/index.html
虛擬機增加一個網卡地址(原機器IP是192.168.56.129)
ifconfig eth0:0 192.168.56.130
建立兩個測試網站
cd home mkdir /website1 #站點目錄 mkdir /website2
將index.html 傳輸到這兩個目錄
配置nginx,增加兩個server節點
# ----server1 ---- server { listen 192.168.56.129:80; server_name www.website1.com; location / { root /website1; index index.html index.htm; } } # ----server2 ---- server { listen 192.168.56.130:80; server_name www.website2.com; location / { root /website2; index index.html index.htm; } }
重啟nginx服務
./nginx -s reload
5.修改本地hosts文件
//hosts文件路徑:windows系統一般在C:\Windows\System32\drivers\etc 192.168.56.129 www.website1.com 192.168.56.130 www.website2.com
通過增加虛擬網卡 、nginx代理 和 修改hosts文件,我在本地就有4個網站(域)可以進行測試了,分別是:
- http://192.168.56.129
- http://192.168.56.130
- http://www.website1.com
- http://www.website2.com
6.測試
準備:
(chrome)打開4個tab,分別進入到上述四個網站,頁面打印了當前origin,通過onclick調用Ajax請求,頁面布局如下
預期:
- http://192.168.56.129 SUCCESS
- http://192.168.56.130 ERROR
- http://www.website1.com ERROR
- http://www.website2.com SUCCESS
結果:
符合預期!
建議使用,除了對域的過濾,還做了其他很多操作,比簡單的自定義過濾器考慮得周全,例如
this.handlePreflightCORS(request1, response, filterChain);
this.handleNonCORS(request1, response, filterChain);
this.handleInvalidCORS(request1, response, filterChain);
總結
cors在開發WebService、RESTful API 時經常會遇到,在以前可能直接通過jsonp解決,jsonp怎樣怎樣就不多說了。 總之,CORS技術規範出來這麽久了,如果不考慮IE6 IE7的問題,那麽還是積極擁抱CORS吧
上文三種解決方案,通過搜索引擎均能找到,但估計大部分都是用的第一種最簡單的無腦的Cors Filter處理,第二種方案是通過nginx配置的,並不適合所有Web應用。第三種,考慮得很周全,而且使用方便,如果不考慮造重復輪子,推薦使用。
本文所用的測試工程代碼太簡單了,就不放github了,直接下載吧,項目下載地址
斷斷續續寫了好幾天。
轉載自:http://www.cnblogs.com/sloong/p/cors.html
CORS跨域實現思路及相關解決方案