CORS跨域以及跨域認證解決方案

由于瀏覽器對于 javascript 的同源策略的限制汇鞭,如果在 A 站點的網(wǎng)頁上希望通過 js 訪問 B 站點的接口、資源等等碌冶,就需要處理跨域問題湿痢。
對于跨域問題,前端有 jsonp(只允許 GET 方法)扑庞、iFrame 等幾種解決方案譬重,例如 jQuery 已經在 ajax 中很好地封裝了 jsonp。
本文介介紹如何在 Spring 項目中實現(xiàn)CORS跨域罐氨。
對 CORS 的介紹請參考 阮一峰的博客 - 跨域資源共享 CORS 詳解臀规。

一、簡介

CORS 是一種 W3C 標準栅隐,全稱為 “Cross-Origin Resource Sharing”塔嬉,即 “跨域資源共享”。它允許瀏覽器向允許跨域的服務器發(fā)出XHR(XMLHttpRequest )請求租悄,以跨域獲取服務或資源谨究,從而跨過 AJAX 請求的同域壁壘。
CORS 需要瀏覽器和服務器同時支持泣棋。目前胶哲,所有瀏覽器都支持該功能,IE 瀏覽器不能低于 IE8潭辈。其中鸯屿,IE8、IE9 并非完全支持標準的 CORS萎胰,需要采用特有的 XDR(XMLDomainRequest)請求碾盟,請參考 此 stackoverflow 頁面
整個 CORS 通信過程都由瀏覽器自動完成技竟,不需要用戶參與冰肴。對于開發(fā)者來說,CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣熙尉。瀏覽器一旦發(fā)現(xiàn) AJAX 請求跨域联逻,就會自動附加相應的頭信息,有時還會多出一次附加的請求检痰,但用戶不會有感覺包归。
因此,實現(xiàn) CORS 通信的關鍵是服務器铅歼。只要服務器實現(xiàn)了 CORS 接口公壤,就可以跨域通信。

二椎椰、 Spring 跨域之 @CrossOrigin 注解方式

4.2以上版本SpringMVC 和 近年大熱的Spring-boot 都支持以 @CrossOrigin 注解方式 讓服務端允許跨域資源共享厦幅。
以 spring-boot 為例,我們首先實現(xiàn)一個簡單的REST服務端慨飘,用來測試對GET和POST請求的跨域訪問:

package com.xiezuozhang.cors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 跨域測試
 * 
 */

@CrossOrigin()
@RestController
@SpringBootApplication
public class CorsTest
{
    public static void main(String[] args) throws Exception {
        SpringApplication.run(CorsTest.class, args);
    }
    
    
    @RequestMapping("/get")
    String testGet() {
        return "Hello World!";
    }
    
    @RequestMapping(value="/post",method=RequestMethod.POST)
    String testPost() {
        return "Hello World!";
    }
    
}

再實現(xiàn)一個簡單的頁面:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test CORS</title>
</head>
<body>


</body>

<script type="text/javascript" src="js/jquery-1.11.3.min.js"></script>
<script type="text/javascript" >

    
    $.ajax({
        type: "GET",
        contentType: 'application/json; charset=utf-8',
        url: "http://localhost:8080/get",
        success: function(data) {
            console.log("Testing cors get:" + data);
        }
    });
    
    
    $.ajax({
        type: "POST",
        contentType: 'application/json; charset=utf-8',
        url: "http://localhost:8080/post",
        success: function(data) {
            console.log("Testing cors post:" + data);
        }
    });
</script>
</html>

服務端跑起來后确憨,用chrome 打開本地頁面,查看執(zhí)行結果:

測試 @CrossOrigin 跨域.png

可以看下瀏覽器的各請求和響應頭:


測試 @CrossOrigin 跨域 GET.png
測試 @CrossOrigin 跨域 POST.png

可以看到Access-Control-Allow-Origin 為“*”瓤的,由前文可知允許所有來源域跨域訪問休弃。
從上面服務端代碼中,我們僅僅使用了@CrossOrigin()圈膏,沒有設置任何參數(shù)塔猾。那么如何配置參數(shù),比如僅允許指定來源域訪問本辐,或者允許跨域請求攜帶認證信息呢桥帆?來看看@CrossOrigin() 的源碼:

@Target({ ElementType.METHOD, ElementType.TYPE })  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface CrossOrigin {  
  
    String[] DEFAULT_ORIGINS = { "*" };  
  
    String[] DEFAULT_ALLOWED_HEADERS = { "*" };  
  
    boolean DEFAULT_ALLOW_CREDENTIALS = true;  
  
    long DEFAULT_MAX_AGE = 1800;  
  
    @AliasFor("origins")  
    String[] value() default {};  
  
    /** 
     * 所有支持的來源域域的集合,例如"http://domain1.com"慎皱。 
     * <p>這些值都顯示在請求頭中的Access-Control-Allow-Origin 
     * "*"代表所有域的請求都支持 
     * <p>如果沒有定義老虫,所有請求的域都支持 
     * @see #value 
     */  
    @AliasFor("value")  
    String[] origins() default {};  
  
    /** 
     * 允許的請求頭,默認都支持 
     */  
    String[] allowedHeaders() default {};  
  
    String[] exposedHeaders() default {};  
  
    /** 
     * 請求支持的方法茫多,例如GET, POST祈匙。 
     * 默認支持RequestMapping中設置的方法 
     */  
    RequestMethod[] methods() default {};  
  
    /** 
     * 是否允許cookie隨請求發(fā)送,使用時必須指定具體的域 
     */  
    String allowCredentials() default "";  
  
    /** 
     * 預請求的結果的有效期天揖,默認30分鐘 
     */  
    long maxAge() default -1;  
  
}  

可見默認允許的來源域設置為 “*”夺欲,默認允許請求攜帶認證參數(shù)。
但是需要注意如果前端跨域請求中確實攜帶了cookie今膊,則來源域就不能設置為“*”些阅,必須與來源域相匹配。修改js代碼測試下:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test CORS</title>
</head>
<body>


</body>

<script type="text/javascript" src="js/jquery-1.11.3.min.js"></script>
<script type="text/javascript" >

    
    $.ajax({
        type: "GET",
        contentType: 'application/json; charset=utf-8',
        url: "http://localhost:8080/get",
        xhrFields: {withCredentials: true},
        success: function(data) {
            console.log("Testing cors get:" + data);
        }
    });
    
    
    $.ajax({
        type: "POST",
        contentType: 'application/json; charset=utf-8',
        url: "http://localhost:8080/post",
        success: function(data) {
            console.log("Testing cors post:" + data);
        }
    });
</script>
</html>

在此我們聲明前一個GET方法攜帶cookie斑唬,而后一個POST方法不攜帶市埋,結果如下:

測試 @CrossOrigin 之攜帶cookie.png

對于前一個請求瀏覽器打印了錯誤信息黎泣,指出了若請求里攜帶了認證信息,則允許的來源域不能直接設為“*”缤谎。而后一個不攜帶cookie的請求則成功執(zhí)行抒倚。

如果我們在服務端設置了匹配的來源域并允許跨域,比如當前我這個html測試示例由于在本地文件中直接打開執(zhí)行坷澡,來源域為“null”:

package com.xiezuozhang.cors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * 跨域測試
 * 
 */

@CrossOrigin(value="null",allowCredentials="true")
@RestController
@SpringBootApplication
public class CorsTest
{
    public static void main(String[] args) throws Exception {
        SpringApplication.run(CorsTest.class, args);
    }
    
    
    @RequestMapping("/get")
    String testGet() {
        return "TEST GET METHOD PASSED";
    }
    
    @RequestMapping(value="/post",method=RequestMethod.POST)
    String testPost() {
        return "TEST POST METHOD PASSED";
    }
    
}

同樣執(zhí)行跨域請求后結果便有所不同了:

修改服務端配置后請求成功.png

看下請求和響應頭:

修改服務端配置后的請求和響應頭.png

那么托呕,如果我們的服務要接受來自多個域的請求,且要求攜帶認證信息频敛,要怎么處理呢项郊?

三、Spring跨域之自定義過濾器或攔截器

上一小節(jié)遺留的問題可以用自定義過濾器或攔截器解決姻政。這里先貼上過濾器實現(xiàn):

package com.xiezuozhang.filter;

import java.io.IOException;
import java.util.List;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;

public class CorsFilter implements ContainerResponseFilter{
    
    private List<String> allowOrigins;
    
    private Boolean allowCredentials;
    
    private Boolean allowAllOrigins = false;


    public List<String> getAllowOrigins() {
        return allowOrigins;
    }

    public void setAllowOrigins(List<String> allowOrigins) {
        this.allowOrigins = allowOrigins;
    }

    public Boolean getAllowCredentials() {
        return allowCredentials;
    }

    public void setAllowCredentials(Boolean allowCredentials) {
        this.allowCredentials = allowCredentials;
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        String allowOrigin = "";
        try {
            String origin = requestContext.getHeaderString("Origin");
            if(null==origin) {
                return;
            }
            if(allowAllOrigins) {
                allowOrigin = origin;
            }else if(null!=allowOrigins && !allowOrigins.isEmpty()) {
                for(String s : allowOrigins) {
                    if(s.trim().equalsIgnoreCase(origin.trim())) {
                        allowOrigin = origin.trim();
                        break;
                    }
                }
            }
            
            responseContext.getHeaders().putSingle("Access-Control-Max-Age", "3600");
            responseContext.getHeaders().putSingle("Access-Control-Allow-Credentials",allowCredentials.toString());
            responseContext.getHeaders().putSingle("Access-Control-Allow-Origin",allowOrigin);
            responseContext.getHeaders().putSingle("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept,Content-Length, Authorization");
            responseContext.getHeaders().putSingle("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,PATCH,OPTIONS");
        }catch(Exception e) {
            e.printStackTrace();
        }
        
    }

    public Boolean getAllowAllOrigins() {
        return allowAllOrigins;
    }

    public void setAllowAllOrigins(Boolean allowAllOrigins) {
        this.allowAllOrigins = allowAllOrigins;
    }

}

以上代碼實現(xiàn)了一個響應鏈上的攔截器呆抑,修改了與跨域相關的響應頭∑裆ぃ現(xiàn)在我們把它加到spring + cxf 實現(xiàn)restfu 風格接口的工程中(由于筆者很早之前就實現(xiàn)了這個測試工程汁展,所以懶得用spring-boot重新寫一遍):

<?xml version="1.0" encoding="UTF-8"?>
<!-- ~ Copyright (c) 2015. Zhejiang Institute Of Public Security Technology 
    Co., Ltd. All Rights Reserved. -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jaxrs="http://cxf.apache.org/jaxrs" xmlns:cxf="http://cxf.apache.org/core"
    xmlns:util="http://www.springframework.org/schema/util" xmlns="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
        http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
        http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
        http://www.springframework.org/schema/util 
        http://www.springframework.org/schema/util/spring-util-4.2.xsd">

    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <cxf:bus>
        <cxf:properties>
            <entry key="org.apache.cxf.jaxrs.bus.providers" value-ref="busProviders" />
        </cxf:properties>
    </cxf:bus>

    <util:list id="busProviders">
        <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider">
        </bean>
        <bean class="com.xiezuozhang.filter.CorsFilter" >
            <property name="allowOrigins" >
                <list>
                    <value>null</value>
                </list>
            </property>
            <property name="allowCredentials" value="true" />
        </bean>
    </util:list>

    <jaxrs:server id="test" address="/test">
        <jaxrs:serviceBeans>
            <bean class="com.xiezuozhang.api.Test" />
        </jaxrs:serviceBeans>
    </jaxrs:server>

</beans>

服務端接口實現(xiàn):

package com.xiezuozhang.api;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class Test {
    
    @GET
    @Path("/{msg}")
    public Map<String,String> test(@PathParam("msg")String msg) {
        Map<String,String> map = new HashMap<>();
        map.put("msg", msg);
        return map;
    }
    
}

網(wǎng)頁端測試實現(xiàn):

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test CORS</title>
</head>
<body>
</body>

<script type="text/javascript" src="js/jquery-1.11.3.min.js"></script>
<script type="text/javascript" >
    $.ajax({
        type: "GET",
        contentType: 'application/json; charset=utf-8',
        url: "http://localhost/corsTest/ws/test/122345",
        xhrFields: {withCredentials: true},
        success: function(data) {
            console.log("Testing cors get:" + data.msg);
        }
    });

</script>
</html>

看下執(zhí)行結果和請求和響應頭:

過濾器跨域測試結果.png
過濾器跨域測試請求和響應頭.png

分析過濾器代碼可以發(fā)現(xiàn),可通過配置 allowOrigins 或 ** allowAllOrigins** 屬性來控制允許的來源域厌殉。具體細節(jié)請自行分析代碼食绿。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市公罕,隨后出現(xiàn)的幾起案子器紧,更是在濱河造成了極大的恐慌,老刑警劉巖楼眷,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铲汪,死亡現(xiàn)場離奇詭異,居然都是意外死亡罐柳,警方通過查閱死者的電腦和手機掌腰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來张吉,“玉大人齿梁,你說我怎么就攤上這事“褂迹” “怎么了勺择?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伦忠。 經常有香客問我省核,道長,這世上最難降的妖魔是什么昆码? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任气忠,我火速辦了婚禮邓深,結果婚禮上,老公的妹妹穿的比我還像新娘笔刹。我一直安慰自己芥备,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布舌菜。 她就那樣靜靜地躺著萌壳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪日月。 梳的紋絲不亂的頭發(fā)上袱瓮,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音爱咬,去河邊找鬼尺借。 笑死,一個胖子當著我的面吹牛精拟,可吹牛的內容都是我干的燎斩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蜂绎,長吁一口氣:“原來是場噩夢啊……” “哼栅表!你這毒婦竟也來了?” 一聲冷哼從身側響起师枣,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤怪瓶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后践美,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洗贰,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年陨倡,在試婚紗的時候發(fā)現(xiàn)自己被綠了敛滋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡玫膀,死狀恐怖矛缨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情帖旨,我是刑警寧澤箕昭,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站解阅,受9級特大地震影響落竹,放射性物質發(fā)生泄漏。R本人自食惡果不足惜货抄,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一述召、第九天 我趴在偏房一處隱蔽的房頂上張望朱转。 院中可真熱鬧,春花似錦积暖、人聲如沸藤为。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缅疟。三九已至,卻和暖如春遍愿,著一層夾襖步出監(jiān)牢的瞬間存淫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工沼填, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桅咆,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓坞笙,卻偏偏與公主長得像岩饼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子羞海,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容