1.情景展示
ajax調取java服務器請求報錯
報錯信息如下:
'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
但是,請求狀態卻是成功:200,這是怎么回事?
2.原因分析
ajax請求跨域:ajax出現請求跨域錯誤問題是因為瀏覽器的“同源策略”。
同源策略:1995年,同源政策由 Netscape 公司引入瀏覽器。目前,所有瀏覽器都實行這個政策。
所謂"同源"指的是"三個相同":協議相同&域名相同&端口相同,這三個要求必須一致,否則就叫跨域。
同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據(比如:同源策略可以限制不同網站cookie共享的問題,通過cors設置照樣可以實現cookie共享)。
同源政策規定,AJAX請求只能發給同源的網址,否則就報錯。
3.ajax解決方案
方法一:JSONP
簡述:網頁通過添加一個<script>元素,向服務器請求JSON數據,這種做法不受同源政策限制;服務器收到請求后,將數據放在一個指定名字的回調函數里傳回來。
但是,只能發送get請求,比較雞肋,其優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。
方法二:WebSocket
WebSocket是一種通信協議,使用ws://(非加密)和wss://(加密)作為協議前綴。該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信。
方法三:CORS(推薦使用)
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標准,是跨源AJAX請求的根本解決方法。相比JSONP只能發GET請求,CORS支持所有類型的HTTP請求。
它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現CORS通信的關鍵是服務器,只要服務器實現了CORS接口,就可以跨源通信。
java服務器設置
第一步:jar包配置
所需jar包:cors-filter-2.8.jar和java-property-utils-1.9.1.jar(這兩個庫文件放到對應項目的WEB-INF/lib/下)
如果是maven項目,將如下代碼添加到pom.xml中
<dependency> <groupId>com.thetransactioncompany</groupId> <artifactId>cors-filter</artifactId> <version>[ version CORS過濾器的最新的穩定版本 ]</version> </dependency>
第二步:添加CORS配置到項目的web.xml中( App/WEB-INF/web.xml)
<!-- 跨域配置CORS--> <filter> <!-- The CORS filter with parameters --> <filter-name>CORSFilter</filter-name> <filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class> <!-- Note: All parameters are options, if omitted the CORS Filter will fall back to the respective default values. --> <!-- 是否允許http請求 --> <init-param> <param-name>cors.allowGenericHttpRequests</param-name> <param-value>true</param-value> </init-param> <!-- 允許跨域的域名(發送請求至該項目的地址、請求源) 構成(http://域名:端口號),比如(http://192.168.191.115:8080) --> <init-param> <param-name>cors.allowOrigin</param-name> <!-- *,表示:允許所有跨域請求,這樣的后果是:當需要客戶端請求攜帶cookie時,瀏覽器無法攜帶cookie至服務器 可以在servlet中動態設置:response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); 這,同樣是允許所有跨域請求,servlet的設置會覆蓋該屬性設置。
但是,當實際測試后發現,這里的*永遠指向的是請求頭的Origin的值,所以不需要再進行額外的設置。 --> <param-value>*</param-value> </init-param> <!-- 允許子域 --> <init-param> <param-name>cors.allowSubdomains</param-name> <param-value>false</param-value> </init-param> <!-- 允許的請求方式(非簡單請求必須添加OPTIONS,因為"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。) --> <init-param> <param-name>cors.supportedMethods</param-name> <param-value>GET, HEAD, POST, OPTIONS,PUT</param-value> </init-param> <!-- 允許的請求頭參數,不能超出范圍 --> <init-param> <param-name>cors.supportedHeaders</param-name> <param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value> </init-param> <!-- 自定義暴露自己的請求頭(自定義設置后Response Headers里會顯示Access-Control-Expose-Headers及值) CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma; 如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定。 --> <init-param> <param-name>cors.exposedHeaders</param-name> <!--這里可以添加一些自己的暴露Headers --> <param-value>X-Test-1, X-Test-2</param-value> </init-param> <!-- 允許客戶端給服務器發送cookie,如果不允許,刪除該屬性即可。(攜帶證書訪問) --> <init-param> <param-name>cors.supportsCredentials</param-name> <param-value>true</param-value> </init-param> <!-- 設定一次預檢請求的有效期,單位為秒;該回應到期前不會再發出另一條預檢請求。 --> <init-param> <param-name>cors.maxAge</param-name> <param-value>3600</param-value> </init-param> </filter> <!-- CORS Filter mapping --> <filter-mapping> <filter-name>CORSFilter</filter-name> <!-- 可自定義設置可供訪問的項目路徑 --> <url-pattern>/*</url-pattern> </filter-mapping>
注意:當web.xml文件中配置了多個filter時,需要將以上配置放在最前面,即:作為第一個filter存在。
4.效果展示
ajax代碼(html需要引入jQuery)
servlet處理
請求完成
沒有添加cors配置前
http的請求頭
ajax的請求頭
經過對比發現,我們會發現兩點不同:
http請求不存在跨域問題,ajax出現跨域會報錯(跨域提醒且無法實現數據交互),輸出在瀏覽器的控制台上;
ajax請求會在Request Headers請求頭會增加一個頭部屬性:Origin,值為當前網頁地址,形如:http://localhost:8070,但是,當瀏覽器檢測出該AJAX請求是跨域請求時,它的值會設置為null。
Origin
字段用來說明:本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。
如果Origin
指定的源,不在許可范圍內,服務器會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息沒有包含Access-Control-Allow-Origin
字段(詳見),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest
的onerror
回調函數捕獲。
注意:這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200(事實上,咱們本次的狀態碼就是200)。
添加cors配置后
ajax可以實現跨域請求,即:Origin
指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段。
Response Headers會多出以下字段:
Origin
字段的值,要么是一個
*
,表示接受任意域名的請求。(必返回項)
注意:如果是非簡單請求,必返回的字段是Access-Control-Request-Method,。
小結:
第一,單純的http請求不存在跨域問題,ajax請求存在跨域問題。(而也是使用java發送http請求至服務器不存在跨域問題的根本原因)
第二,跨域請求,請求頭會自動加上Origin字段;當服務器添加cors配置后,響應頭必返回Access-Control-Allow-Origin字段;
如果是非簡單請求可能會返回:Access-Control-Request-Method,表示:服務器端允許接受的http請求方式(當請求數據格式指定為application/json時,並沒有出現該字段,按理說該出來的)。
4.如何復現跨域問題?
我們現在知道:協議、域名、端口三者只要有一個不同,就叫跨域。那么,要重現跨域場景,就需要兩台服務器,即同一台電腦,跑兩個tomcat服務器即可(兩個tomcat的端口號必須不同,否則端口沖突)。
這里只介紹最簡單的一種跨域請求方式:
將ajax代碼寫到一個單獨的html文件中,運行你的tomcat服務器, 使用瀏覽器打開該網頁文件,就可重現ajax跨域問題啦。
一旦服務器通過了"預檢"請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin
頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin
頭信息字段。
如上圖所示,重現跨域問題后,按照第三步的cors方案,就能解決跨域問題啦。
5.瀏覽器如何向服務器跨域傳送cookie?
錯誤示例:
本機運行了兩個項目對應兩個tomcat,端口號分別是8080和8070,
如上圖所示,Host代表的是服務端,Origin代表的是客戶端,兩者的域名不同,其結果就是:
雖然可以實現跨域,但是,無法實現cookie共享(服務端照樣可以返回cookie且瀏覽器能接收到,但是,瀏覽器發送請求至服務器時卻無法攜帶cookie)。
由此可見,當域名完全不一致時,cookie無法實現跨域(子域不同的情況我沒有進行測試)。
這讓我誤以為,cookie跨域共享是無法實現的,但是,事實並非如此。
正確示例:
localhost:8080的服務器也能接收到實際的數據
打印結果
具體實現:
window.onload = function(){ // 前端往項目當中添加cookie document.cookie = "name=Marydon;path=/"; $.ajax({ url:'http://localhost:8080/test/crossServlet', method:'post', xhrFields: { withCredentials: true },// cookie能夠傳過去的關鍵所在 /*crossDomain: true,*/ data:{name:'張三'}, success:function(data) { alert(data); } }); }
前端設置:關鍵就在於,ajax需要添加參數:xhrFields:{withCredentials:true};
后台設置:(添加響應頭部信息設置,response.setHeader())
配置Access-Control-Allow-Credentials,值為true,對應cors的cors.supportsCredentials;
配置Access-Control-Allow-Origin,值為不能為 '*',對應cors的cors.allowOrigin,但經過實踐發現,其值為*時並沒有產生影響。
說明:
在前端添加cookie時,必須設置路徑,不然,cookie只作用於當前頁面;
在默認情況下,只有設置 cookie的網頁才能讀取該 cookie。如果想讓一個頁面讀取另一個頁面設置的cookie,必須設置cookie的路徑。
cookie的路徑用於設置能夠讀取 cookie的頂級目錄。將這個路徑設置為網站的根目錄(/),這樣所有網頁都能讀取到該cookie,
這也是js設置cookie后,后台取不到值的原因。
另外,在前端是獲取不到JSESSIONID這個cookie的,因為它設置了httpOnly屬性,即:只有后台能夠得到該cookie。
小結:
cookie的跨域共享不是無條件的,即:請求和響應的IP完全不相同時,無法實現cookie共享,這就相當於A網站不能訪問B網站的cookie一個道理。
當請求發起方和接收方的域名(IP)完全一致,端口號不同時,瀏覽器是可以攜帶cookie的,也就是:服務器能接收到前端所傳來的cookie。
當IP的頂級域名相同時,沒有進行測試。
經過上述實踐發現:跨域cookie共享的局限性很大,還不如不用,有實際使用場景的大佬,歡迎留言。