CORS跨域以及跨域認(rèn)證解決方案

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

一、簡(jiǎn)介

CORS 是一種 W3C 標(biāo)準(zhǔn),全稱為 “Cross-Origin Resource Sharing”,即 “跨域資源共享”。它允許瀏覽器向允許跨域的服務(wù)器發(fā)出XHR(XMLHttpRequest )請(qǐng)求,以跨域獲取服務(wù)或資源,從而跨過 AJAX 請(qǐng)求的同域壁壘。
CORS 需要瀏覽器和服務(wù)器同時(shí)支持。目前,所有瀏覽器都支持該功能,IE 瀏覽器不能低于 IE8。其中,IE8、IE9 并非完全支持標(biāo)準(zhǔn)的 CORS,需要采用特有的 XDR(XMLDomainRequest)請(qǐng)求,請(qǐng)參考 此 stackoverflow 頁面。
整個(gè) CORS 通信過程都由瀏覽器自動(dòng)完成,不需要用戶參與。對(duì)于開發(fā)者來說,CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn) AJAX 請(qǐng)求跨域,就會(huì)自動(dòng)附加相應(yīng)的頭信息,有時(shí)還會(huì)多出一次附加的請(qǐng)求,但用戶不會(huì)有感覺。
因此,實(shí)現(xiàn) CORS 通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了 CORS 接口,就可以跨域通信。

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

4.2以上版本SpringMVC 和 近年大熱的Spring-boot 都支持以 @CrossOrigin 注解方式 讓服務(wù)端允許跨域資源共享。
以 spring-boot 為例,我們首先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的REST服務(wù)端,用來測(cè)試對(duì)GET和POST請(qǐng)求的跨域訪問:

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;

/**
 * 跨域測(cè)試
 * 
 */

@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!";
    }
    
}

再實(shí)現(xiàn)一個(gè)簡(jiǎ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>

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

測(cè)試 @CrossOrigin 跨域.png

可以看下瀏覽器的各請(qǐng)求和響應(yīng)頭:


測(cè)試 @CrossOrigin 跨域 GET.png
測(cè)試 @CrossOrigin 跨域 POST.png

可以看到Access-Control-Allow-Origin 為“*”,由前文可知允許所有來源域跨域訪問。
從上面服務(wù)端代碼中,我們僅僅使用了@CrossOrigin(),沒有設(shè)置任何參數(shù)。那么如何配置參數(shù),比如僅允許指定來源域訪問,或者允許跨域請(qǐng)求攜帶認(rèn)證信息呢?來看看@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>這些值都顯示在請(qǐng)求頭中的Access-Control-Allow-Origin 
     * "*"代表所有域的請(qǐng)求都支持 
     * <p>如果沒有定義,所有請(qǐng)求的域都支持 
     * @see #value 
     */  
    @AliasFor("value")  
    String[] origins() default {};  
  
    /** 
     * 允許的請(qǐng)求頭,默認(rèn)都支持 
     */  
    String[] allowedHeaders() default {};  
  
    String[] exposedHeaders() default {};  
  
    /** 
     * 請(qǐng)求支持的方法,例如GET, POST。 
     * 默認(rèn)支持RequestMapping中設(shè)置的方法 
     */  
    RequestMethod[] methods() default {};  
  
    /** 
     * 是否允許cookie隨請(qǐng)求發(fā)送,使用時(shí)必須指定具體的域 
     */  
    String allowCredentials() default "";  
  
    /** 
     * 預(yù)請(qǐng)求的結(jié)果的有效期,默認(rèn)30分鐘 
     */  
    long maxAge() default -1;  
  
}  

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

<!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>

在此我們聲明前一個(gè)GET方法攜帶cookie,而后一個(gè)POST方法不攜帶,結(jié)果如下:

測(cè)試 @CrossOrigin 之?dāng)y帶cookie.png

對(duì)于前一個(gè)請(qǐng)求瀏覽器打印了錯(cuò)誤信息,指出了若請(qǐng)求里攜帶了認(rèn)證信息,則允許的來源域不能直接設(shè)為“*”。而后一個(gè)不攜帶cookie的請(qǐng)求則成功執(zhí)行。

如果我們?cè)诜?wù)端設(shè)置了匹配的來源域并允許跨域,比如當(dāng)前我這個(gè)html測(cè)試示例由于在本地文件中直接打開執(zhí)行,來源域?yàn)椤皀ull”:

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;

/**
 * 跨域測(cè)試
 * 
 */

@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í)行跨域請(qǐng)求后結(jié)果便有所不同了:

修改服務(wù)端配置后請(qǐng)求成功.png

看下請(qǐng)求和響應(yīng)頭:

修改服務(wù)端配置后的請(qǐng)求和響應(yīng)頭.png

那么,如果我們的服務(wù)要接受來自多個(gè)域的請(qǐng)求,且要求攜帶認(rèn)證信息,要怎么處理呢?

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

上一小節(jié)遺留的問題可以用自定義過濾器或攔截器解決。這里先貼上過濾器實(shí)現(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;
    }

}

以上代碼實(shí)現(xiàn)了一個(gè)響應(yīng)鏈上的攔截器,修改了與跨域相關(guān)的響應(yīng)頭?,F(xiàn)在我們把它加到spring + cxf 實(shí)現(xiàn)restfu 風(fēng)格接口的工程中(由于筆者很早之前就實(shí)現(xiàn)了這個(gè)測(cè)試工程,所以懶得用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>

服務(wù)端接口實(shí)現(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)頁端測(cè)試實(shí)現(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í)行結(jié)果和請(qǐng)求和響應(yīng)頭:

過濾器跨域測(cè)試結(jié)果.png
過濾器跨域測(cè)試請(qǐng)求和響應(yīng)頭.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容