cors跨域之簡單請求與預檢請求(發送請求頭帶令牌token)

引子

自從從JAVA僞全棧轉前端以來,學習的路上就充滿了荊棘(奇葩問題),而涉及先後端分離這個問題,對cors的應用不斷增多,暴露出的問題也接踵而至。
這兩天動手實踐基於Token的WEB後臺認證機制,看過諸多理論(較好一篇推薦),正所謂慮一千次,不如去作一次。 猶豫一萬次,不如實踐一次,因此就有了下文,關於token的生成,另一篇文章會細講,本篇主要討論在發送ajax請求,頭部帶上自定義token驗證驗證,暴露出的跨域問題。html

先說說定義

CORS:跨來源資源共享(CORS)是一份瀏覽器技術的規範,提供了 Web 服務從不一樣網域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,是 JSONP 模式的現代版。與 JSONP 不一樣,CORS 除了 GET 要求方法之外也支持其餘的 HTTP 要求。用 CORS 可讓網頁設計師用通常的 XMLHttpRequest,這種方式的錯誤處理比JSONP要來的好,JSONP對於 RESTful 的 API 來講,發送 POST/PUT/DELET 請求將成爲問題,不利於接口的統一。但另外一方面,JSONP 能夠在不支持 CORS 的老舊瀏覽器上運做。不過現代的瀏覽器(IE10以上)基本都支持 CORS。
預檢請求(option):在 CORS 中,可使用 OPTIONS 方法發起一個預檢請求(通常都是瀏覽檢測到請求跨域時,會自動發起),以檢測實際請求是否能夠被服務器所接受。預檢請求報文中的 Access-Control-Request-Method 首部字段告知服務器實際請求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服務器實際請求所攜帶的自定義首部字段。服務器基於從預檢請求得到的信息來判斷,是否接受接下來的實際請求。前端

OPTIONS /resources/post-here/ HTTP/1.1 
Host: bar.other 
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 
Accept-Language: en-us,en;q=0.5 
Accept-Encoding: gzip,deflate 
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 
Connection: keep-alive 
Origin: http://foo.example 
Access-Control-Request-Method: POST 
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

服務器所返回的 Access-Control-Allow-Methods 首部字段將全部容許的請求方法告知客戶端。該首部字段與 Allow 相似,但只能用於涉及到 CORS 的場景中。vue

問題描述

話很少說,先上代碼:java

前端(ajax庫:vue-resource)
        userLogin:function(){
            this.$http({
                method:'post',
                url:'http://localhost:8089/StockAnalyse/LoginServlet',
                params:{"flag":"ajaxlogin","loginName":this.userInfo.id,"loginPwd":this.userInfo.psd}, 
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 
                credientials:false, 
                emulateJSON: true                    
            }).then(function(response){
                sessionStorage.setItem("token",response.data);
                this.isActive =false;
                document.querySelector("#showInfo").classList.toggle("isLogin");
            })                 
        }
後端相關配置:
        response.setHeader("Access-Control-Allow-Origin", "http://localhost"); //容許來之域名爲http://localhost的請求        
    response.setHeader("Access-Control-Allow-Headers", "Origin,No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, userId, token");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); //請求容許的方法
    response.setHeader("Access-Control-Max-Age", "3600");    //身份認證(預檢)後,xxS之內發送請求不在須要預檢,既能夠直接跳過預檢,進行請求(前面只是照貓畫虎,後面才理解)

關於上面一段代碼,是個人用戶首次登陸認證,生成token令牌,保存在sessionStorage中,供後面調用;須要說明的是,前端服務器地址是:localhost:80,後端服務器地址:localhost:8089,因此先後端涉及到跨域,本身在後端作了相應的跨域設置:response.setHeader("Access-Control-Allow-Origin", "http://localhost"); 因此登陸認證,安全的實現了跨域信息認證,後端相應發送回來了相應的token信息。
但獲取到token後,想在須要的時候,在請求的頭部攜帶上這個令牌,來作相應的身份認證,因此本身在請求中作了這些改動(有標註),後端沒改動,源碼:web

checkIdentity:function(){
            let token =sessionStorage.getItem('token');
            this.$http({
                method:'post',
                url:'http://localhost:8089/StockAnalyse/LoginServlet',
                params:{"flag":"checklogin","isLogin":true,"token":token}, 
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 
                headers:{'token':token},        //header中攜帶令牌信息            
                credientials:false, 
                emulateJSON: true                    
            }).then(function(response){
                console.log(response.data);
            })                 
        }

但實際上在devtools打印了以下錯誤信息:Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.仔細想想,好像,彷佛這個問題遇到過,還提過問,確實提過,連接在這裏。但此次的設置和上次同樣,就在header裏多加了一個自定義token,但卻報了和上次沒有設置headers: {'Content-Type': 'application/x-www-form-urlencoded'}同樣的錯誤信息,因而,不知所措,算了,重頭再來,好好百度,研究一下cors跨域。ajax

理論學習

運氣不錯,找到了一篇好文,文章講的很細,也找到本身問題的所在:觸發 CORS 預檢請求。引用原文的話加以本身總結:跨域資源共享標準新增了一組 HTTP 首部字段,容許服務器聲明哪些源站有權限訪問哪些資源。另外,規範要求,對那些可能對服務器數據產生反作用的 HTTP 請求方法(特別是 GET 之外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request:似曾相識有沒有?誒,對,上面那個錯誤信息中,就有一個這樣陌生的詞彙),從而獲知服務端是否容許該跨域請求。服務器確認容許以後,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也能夠通知客戶端,是否須要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關數據)。因此跨域請求分兩種:簡單請求和預檢請求。一次完整的請求不須要服務端預檢,直接響應的,歸爲簡單請求;而響應前須要預檢的,稱爲預檢請求,只有預檢請求經過,纔有接下來的簡單請求。對於那些是簡單請求,那些會觸發預檢請求,文章作了詳細的總結,這裏列出觸發預檢請求的條件(不知道腦子爲啥會想到那些會觸發BFC的條件),不要跑題,原文是這樣總結的:apache

當請求知足下述任一條件時,即應首先發送預檢請求:
使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
人爲設置了對 CORS 安全的首部字段集合以外的其餘首部字段。該集合爲:
Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below)
DPR
Downlink
Save-Data
Viewport-Width
Width
 Content-Type 的值不屬於下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain

問題分析

因此,再來看本身兩次犯錯(第一次是沒有設置:headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 第二次是設置自定義header,headers:{'token':token}。很巧,有沒有,一次少,一次多,都點燃了導火索),其實都是觸發了預檢請求。對於第一次的錯誤,很好解決,增長headers: {'Content-Type': 'application/x-www-form-urlencoded'},就解決了,關於Conten-Type的幾種取值,你須要知道的。但對於第二個錯誤,好像無法向第一種那樣,將預檢請求轉變爲簡單請求,因此,只有尋找方法怎麼在後端實現相應的預檢請求,來返回一個狀態碼2xx,告訴瀏覽器這次跨域請求能夠繼續。因此注意力轉向後端。
關於JAVA實現預檢請求,基本都是採用過濾器,不要問我爲何不是監聽器或者攔截器(我就是個僞全棧,就不要相互爲難了,本身百度之),自定義(copy)了一個filter,並在web.xml中進行了設置。源碼:segmentfault

Filter接口實現部分:
package stock.model;
import java.io.IOException;   
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 org.apache.commons.httpclient.HttpStatus;   //這裏須要添加commons-httpclient-3.1.jar
public class CorsFilter implements Filter {     //filter 接口的自定義實現
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        response.setHeader("Access-Control-Allow-Origin", "*");           
        String token = request.getHeader("token");
        System.out.println("filter origin:"+token);//經過打印,能夠看到一次非簡單請求,會被過濾兩次,即請求兩次,第一次請求確認是否符合跨域要求(預檢),這一次是不帶headers的自定義信息,第二次請求會攜帶自定義信息。
        if ("OPTIONS".equals(request.getMethod())){//這裏經過判斷請求的方法,判斷這次是不是預檢請求,若是是,當即返回一個204狀態嗎,標示,容許跨域;預檢後,正式請求,這個方法參數就是咱們設置的post了
          response.setStatus(HttpStatus.SC_NO_CONTENT); //HttpStatus.SC_NO_CONTENT = 204
          response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS, DELETE");//當斷定爲預檢請求後,設定容許請求的方法
          response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, Token"); //當斷定爲預檢請求後,設定容許請求的頭部類型
          response.addHeader("Access-Control-Max-Age", "1");  // 預檢有效保持時間                       
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {     
    }
}
web.xml配置部分
<filter>
<filter-name>cors</filter-name>
<filter-class>stock.model.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

關於Access-Control-Max-Age

最近又開始寫java,再回來看這個,發現當時Access-Control-Max-Age設置了1.其實這樣寫有很大問題,由於每一個複雜請求都會發兩次。顯然這樣是當代所不能接受的,因此Max-Age的值適合設的大一些,具體多大很業務需求相關。另外Access-Control-Max-Age不是針對請求域名有效的,是請求的完成路徑有效的,好比第一次發出www.exanple.com/api/corsGet,會產生一次options請求和一次post請求,而後我再請求一次,這時沒有預檢請求了,只有post請求。但再發送一次www.exanple.com/api/corsSave請求,會發現又產生了一次options請求和一次post請求,因此Access-Control-Max-Age不是針對相同的origin有效,而是針對相同的requestUrl有效。很重要哦。後端

結論

當在後端實現添加上面的源碼後,皆大歡喜,問題得以解決,補上失敗和成功,本身截下的兩張請求響應圖。圖片描述仔細看請求響應失敗發起響應那張圖,在General的數據集中,能夠看到方法是options,而非代碼指定的post請求,因此這是一次瀏覽器發出的一次預檢請求,讓服務器確認此IP是否有訪問的權限,若是有,服務器須要返回一個2xx的狀態碼給瀏覽器。緊接着再發起一次簡單請求。以下面在devtools中的截取圖片(爲了對比清除,我把兩次分別截取,作了拼接,由於不會作動態圖)。能夠看到同一個post請求,實際上產生了兩次網絡鏈接。圖片描述
但關於cors,要去探索的,還有不少不少,因此遵循革命語錄:實踐(有時也能夠是時間)是檢驗真理的惟一標準,是沒有錯的。後續有新的收穫,再補充。api

相關文章
相關標籤/搜索