前后端分離之CORS跨域訪問踩坑總結

前言

前后端分離的開發模式越來越流行,目前絕大多數的公司與項目都采取這種方式來開發,它的好處是前端可以只專注于頁面實現,而后端則主要負責接口開發,前后端分工明確,彼此職責分離,不再高度耦合,但是由于這種開發模式將前后端項目分開來獨立部署,所以將必不可免的會碰到跨域問題.
什么是跨域
跨域指的是瀏覽器不能執行其他網站的腳本.它是由瀏覽器的同源策略造成的,是瀏覽器對javascript施加的安全限制.目前所有的瀏覽器都實行同源策略,所謂的同源指的是

  • 協議相同
  • 域名相同
  • 端口相同

是否跨域判定如下:
http://www.123.com/index.html 調用 http://www.123.com/server.php(非跨域)
http://www.123.com/index.html 調用 http://www.456.com/server.php(主域名不同:123/456,跨域)
http://abc.123.com/index.html 調用 http://def.123.com/server.php(子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 調用 http://www.123.com:8081/server.php(端口不同:8080/8081,跨域)
http://www.123.com/index.html 調用 https://www.123.com/server.php(協議不同:http/https,跨域)
請注意:localhost和127.0.0.1雖然都指向本機,但也屬于跨域.
瀏覽器執行javascript腳本時,會檢查這個腳本屬于哪個頁面,如果不是同源頁面,就不會被執行.

如何解決

常見的解決方案有JSONP和CORS等多種方式,這里我們采用了CORS的方式來統一處理項目中的跨域問題.
cors是一種w3c標準,全稱是"跨域資源共享(Cross-origin resource sharing)".它允許瀏覽器向跨源服務器發出XMLHttpRequest請求,從而克服了同源使用的限制.下面重點來看下我們在SpringBoot項目中使用cors處理跨域時所遇到的問題.
首先創建一個WebMvcConfig類繼承自WebMvcConfigurerAdapter類并覆寫其中的addCorsMappings方法

    /**
     * 允許跨域訪問
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Accept", "Origin", "X-Requested-With", "Content-Type",
                        "Last-Modified", "device", "token")
                .exposedHeaders("Set-Cookie")
                .allowCredentials(true).maxAge(3600);

    }

接著我們在之前搭建好的Vue項目環境中創建一個vue文件,用來向springboot項目發起跨域請求.

<template>
   <div>
       <input v-model="message" placeholder="edit me"/>
       <p>Message is: {{ message }}</p>
       <input type="button" @click="get()" value="測試">
   </div>
</template>
<script>
  export default {
    data () {
       return {
          message: 'This is my from'
       }
    },
    methods:{
        get:function(){          
            $.ajax({
        url:'http://localhost:8080/win/api/test/cors',//測試接口
        type:'post',
        beforeSend:(xhr) => {
                        xhr.setRequestHeader("token", "web_session_key-082ba2a3-7d8e-407c-8bd0-2fbc430b0dbf");
            xhr.setRequestHeader("device","APP");
                 },
         success:function(data){
              console.log(data);
         }
        });
        }
    }
}
</script>

然后啟動前端vue項目

npm run dev

打開瀏覽器輸入http://localhost:8081/點擊頁面中的測試按鈕發起接口調用,注意vue工程的端口為8081,springboot工程的端口為8080,不符合同源規則所以訪問后端接口時會出現跨域.此時打開chrome控制臺會發現

image.png

通過chrome控制臺查看接口請求的headers信息是這樣的
image.png

可以看到這個接口的Request Method為OPTIONS,而不是我們在ajax代碼中所設置的POST,仔細看控制臺報的異常Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.可以發現其中的關鍵字preflight(預檢請求),這個其實就是問題的所在了,再次google了一下cors的相關知識,才知道原來瀏覽器將cors請求分為兩類即簡單請求和非簡單請求.
簡單請求
只要同時滿足以下條件就屬于簡單請求
請求方法是以下三種方法之一:GET POST HEAD
Http的頭信息不超出以下幾種字段:Accept Accept-Language Content-Language Last-Event-ID Content-Type 只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
非簡單請求
其他的請求皆屬于非簡單請求.
注意在cors定義中,如果頭信息中的Content-Type不設置,則默認值為json/application,如果Content-Type值不為application/x-www-form-urlencoded,multipart/form-data或text/plain,都被視為非簡單請求,即預檢請求.
主要問題描述
瀏覽器對這兩種請求的處理是不一樣的.
如果是簡單請求的話,一次完整的請求過程是不需要服務端預檢的,直接響應回客戶端,而非簡單請求則瀏覽器會在發送真正請求之前先用OPTIONS發送一次預檢請求preflight request,從而獲知服務端是否允許該跨域請求,當服務器確認允許之后,才會發起真正的請求.那么前面所出現的異常很明顯就是由這個preflight request導致的了,回頭看代碼可發現我們在發起ajax調用時往請求頭里面塞了兩個自定義的header參數token和device,所以此次調用屬于非簡單請求,并觸發了一次預檢請求,由于我們服務端使用了shiro權限認證框架,通過攔截用戶請求頭中傳過來的token信息來判定客戶端是否為非法調用,而Preflight請求過程中并不會攜帶我們自定義的token信息到服務器,這樣服務器校驗就永遠也無法通過了,就算是合法的登錄用戶也會被攔截.
解決的辦法
既然已經知道原因了,那么自然解決這個問題的辦法就是交給后端了,在后端檢測到該請求為預檢請求時,不讓它繼續往下走(也可以返回一個2xx的狀態碼),直接告訴瀏覽器此次跨域請求可以繼續,很明顯過濾器符合我們的要求,我們來把之前的addCorsMappings方法去掉,重寫這塊代碼,在網上查了很久,嘗試了很多種方法,得出來的結論大致有如下兩種方式:
第一種方案采用過濾器的機制

    @Bean
    public FilterRegistrationBean corsFilter() {
        return new FilterRegistrationBean(new Filter() {
            public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                    throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) req;
                HttpServletResponse response = (HttpServletResponse) res;
                String method = request.getMethod();
                // this origin value could just as easily have come from a database
                response.setHeader("Access-Control-Allow-Origin", "*");
                response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Credentials", "true");
                response.setHeader("Access-Control-Allow-Headers", "Accept, Origin, X-Requested-With, Content-Type,Last-Modified,device,token");
                if ("OPTIONS".equals(method)) {//檢測是options方法則直接返回200
                    response.setStatus(HttpStatus.OK.value());
                } else {
                    chain.doFilter(req, res);
                }
            }

            public void init(FilterConfig filterConfig) {
            }

            public void destroy() {
            }
        });
    }

第二種方案
1.創建一個類MyCorsRegistration繼承自CorsRegistration

public class MyCorsRegistration extends CorsRegistration {

    public MyCorsRegistration(String pathPattern) {
        super(pathPattern);
    }

    @Override
    public CorsConfiguration getCorsConfiguration() {
        return super.getCorsConfiguration();
    }
}

2.然后在WebMvcConfig類中增加一個方法里來處理跨域問題.

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        // 對響應頭進行CORS授權
        MyCorsRegistration corsRegistration = new MyCorsRegistration("/**");
        corsRegistration.allowedOrigins("*")
                .allowedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name(),
                        HttpMethod.PUT.name(), HttpMethod.OPTIONS.name())
                .allowedHeaders("Accept", "Origin", "X-Requested-With", "Content-Type",
                        "Last-Modified", "device", "token")
                .exposedHeaders(HttpHeaders.SET_COOKIE)
                .allowCredentials(true)
                .maxAge(3600);

        // 注冊CORS過濾器
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
        configurationSource.registerCorsConfiguration("/**", corsRegistration.getCorsConfiguration());
        CorsFilter corsFilter = new CorsFilter(configurationSource);
        return new FilterRegistrationBean(corsFilter);
    }

其實第二種方案本質上也是過濾器的機制,看源碼可知CorsFilter繼承自spring的過濾器OncePerRequestFilter,來看看它的doFilterInternal方法

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (CorsUtils.isCorsRequest(request)) {
            CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
            if (corsConfiguration != null) {
                boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
                if (!isValid || CorsUtils.isPreFlightRequest(request)) {
                    return;
                }
            }
        }

        filterChain.doFilter(request, response);
    }

代碼大致意思是先判斷是否為cors請求再判斷是否預檢請求,符合條件則直接return了.
我們來看看最后的請求結果,控制臺再也沒有報錯了,瀏覽器一共發送了兩次接口請求,第一次是OPTIONS請求,第二次是POST請求.
OPTIONS請求:


image.png

POST請求:


image.png

從上面第一個圖中可以看到這個預檢請求主要攜帶的請求header信息如下(并沒有攜帶我們自定義的header信息,第二個圖中可以清楚的看到我們自定義的header信息了):
Access-Control-Request-Headers:device,token 真實請求攜帶的Header中的信息
Access-Control-Request-Method:POST 我真實請求的方法是什么
Origin:http://localhost:8081 告訴服務器我的域名
然后是服務器端給我們返回的Preflight Response:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Accept, Origin, X-Requested-With, Content-Type,Last-Modified,device,token
Access-Control-Allow-Methods:GET, HEAD, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Origin:*
Access-Control-Max-Age:3600
值得注意的是上面的Access-Control-Max-Age這個header,它告訴了服務器在多少秒內,不需要再發送預檢請求,可以緩存該結果,即只發送真正的請求,不同瀏覽器有不同的上限.在Firefox中,上限是24h(即86400秒),而在Chrome中則是10min(即600秒).Chrome同時規定了一個默認值5秒.如果值為-1,則表示禁用緩存,每一次請求都需要提供預檢請求,即用OPTIONS請求進行檢測.它對完全一樣的url的緩存設置生效,多一個參數也視為不同url.也就是說,如果設置了10分鐘的緩存,在10分鐘內,所有請求第一次會產生options請求,第二次以及第二次以后就只發送真正的請求了,這也是一個很有效的性能優化手段(另外注意下在chrome瀏覽器中如果開啟了Disable cache,則表示本地不緩存,會導致每次請求都發一次預檢測).

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容