Spring Boot - 跨域資源共享(CORS)

同源策略

在瀏覽器中蒜胖,如果我們直接使用 AJAX 發(fā)送一個對其他網(wǎng)站的請求(跨域請求)督惰,默認情況下是無法獲取到響應(yīng)的采呐。
這是因為瀏覽器內(nèi)置的 同源策略 對客戶端腳本的限制。

默認情況下吼畏,同源策略 只允許腳本請求同源資源督赤,而對于請求不同源的腳本在沒有明確授權(quán)的情況下,無法讀取對方資源泻蚊。

同源 指的是:協(xié)議域名端口 三者都相同

同源策略是瀏覽器內(nèi)置的一個最核心丑婿,也是最基礎(chǔ)的安全功能性雄,它保障了用戶的上網(wǎng)安全。

但是羹奉,如果我們確信某個非同源網(wǎng)站是安全的秒旋,我們希望能夠?qū)ζ滟Y源進行訪問,那么诀拭,就需要通過相應(yīng)的機制進行跨域請求迁筛。

最常見的前端跨域請求解決方案是 JSONP,它的原理是借助script標簽不受瀏覽器同源策略限制耕挨,允許跨域請求資源细卧,因此可以通過script標簽的src屬性,進行跨域訪問筒占。如下代碼所示:

// 1. 前端定義一個 回調(diào)函數(shù) handleResponse 用來接收后端返回的數(shù)據(jù)
function handleResponse(data) {
    console.log(data);
};

// 2. 動態(tài)創(chuàng)建一個 script 標簽贪庙,并且告訴后端回調(diào)函數(shù)名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);

// 3. 通過 script.src 請求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 后端能夠識別這樣的 URL 格式并處理該請求翰苫,然后返回 handleResponse({"name": "laixiangran"}) 給瀏覽器
// 5. 瀏覽器在接收到 handleResponse({"name": "laixiangran"}) 之后立即執(zhí)行 止邮,也就是執(zhí)行 handleResponse 方法这橙,獲得后端返回的數(shù)據(jù),這樣就完成一次跨域請求了导披。

雖然 JSONP 可以完成跨域請求屈扎,但是它只支持GET請求方式,限制非常大撩匕。
于是鹰晨,為了更好地支持跨域資源請求,W3C 標準就發(fā)布了一套瀏覽器跨域資源共享標準:CORS(Cross-origin resource sharing滑沧,跨域資源共享)

CORS(跨域資源共享)

CORS 支持多種 HTTP 請求并村,它其實就是定義了一套跨域資源請求時,瀏覽器與服務(wù)器之間的交互方式滓技×梗基本的原理就是通過自定義的 HTTP 請求頭來傳遞信息,進行驗證令漂。

瀏覽器中膝昆,將 CORS 請求分為兩種類型:

  • 簡單請求:同時滿足以下兩大條件的請求,即為簡單請求:

    1. 請求的方法是HEAD叠必、GET或者是POST三種之一
    2. 請求頭不超出以下幾種字段:Accept荚孵、Accept-LanguageContent-Language纬朝、Last-Event-ID收叶、Content-Type(其值為:application/x-www-form-urlencodedmultipart/form-datatext/plain三者之一)
  • 非簡單請求:不是簡單請求的都屬于非簡單請求共苛。

瀏覽器對于 簡單請求 和 非簡單請求 的 CORS 處理機制不一樣判没,具體如下:

  • 簡單請求:對于簡單請求的 CORS,瀏覽器的處理機制流程如下:

    1. 瀏覽器會在請求頭添加一個額外的Origin頭部隅茎,其值為當前請求頁面的源信息(即:協(xié)議 + 域名 + 端口)澄峰。如下所示:
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    
    1. 服務(wù)器接收到請求后,查看到Origin頭部指定的源信息辟犀,如果同意該請求俏竞,就會為下發(fā)的響應(yīng)添加頭部Access-Control-Allow-Origin,其值為請求的源信息(或者是*堂竟,表示允許任意源信息)魂毁。如下所示:
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8
    
    1. 瀏覽器接收到響應(yīng)后,會查看下是否有Access-Control-Allow-Origin頭部信息跃捣,如果沒有或者其值不匹配當前源信息漱牵,那么瀏覽器就會禁止響應(yīng)該 CORS 請求,當前頁面的 AJAX 請求的onerror函數(shù)會得到回調(diào)疚漆。
      反之酣胀,如果瀏覽器驗證通過刁赦,則跨域請求成功。

    :CORS 請求默認不發(fā)送 Cookie 和 HTTP 認證信息闻镶,如果需要把 Cookie 發(fā)送給服務(wù)器甚脉,則 AJAX 和 服務(wù)器必須同時打開 Credentials 字段,如下所示:

    • 服務(wù)器需設(shè)置:Access-Control-Allow-Credentials: true
    • AJAX 需設(shè)置:new XMLHttpRequest().withCredentials = true;

    :如果 AJAX 發(fā)送了 Cookie铆农,那么服務(wù)器的Access-Control-Allow-Origin則不能設(shè)置為*牺氨,必須指定該明確的、與請求網(wǎng)頁一致的域名墩剖。

  • 非簡單請求:非簡單請求是那種對服務(wù)器有特殊要求的請求猴凹,比如PUTDELETE請求,或者是Content-type: application/json請求...
    瀏覽器檢測到非簡單請求的 CORS 時岭皂,在正式發(fā)送請求前郊霎,會先進行一次探測請求(preflight),通過才會發(fā)送正式請求爷绘,具體過程如下:

    1. 瀏覽器檢測到非簡單 CORS 請求书劝,則先發(fā)送一個探測請求,請求方式為OPTIONS土至,如下所示:
    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    可以看到OPTIONS請求购对,除了攜帶Origin請求頭外,還額外攜帶了以下幾個請求頭:

    • Access-Control-Request-Method:該字段必須攜帶陶因,表示 CORS 請求使用的 HTTP 請求方法
    • Access-Control-Request-Headers:可選字段骡苞,表示 CORS 請求發(fā)送的自定義頭部信息,多個頭部以逗號進行分隔
    1. 服務(wù)器收到瀏覽器發(fā)送的探測請求后楷扬,檢測Origin烙如、Access-Control-Request-MethodAccess-Control-Request-Headers都在自己的許可名單時,就會允許跨域請求毅否,返回響應(yīng)。如下所示:
    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Max-Age: 1728000
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain
    

    響應(yīng)主要包含如下請求頭信息:

    • Access-Control-Allow-Origin:表示允許進行跨域請求的域
    • Access-Control-Allow-Methods:必須字段蝇刀,表示允許 CORS 請求的方法
    • Access-Control-Allow-Headers:表示允許 CORS 請求的頭部
    • Access-Control-Max-Age:表示探測請求緩存時間(單位:秒)
    1. 一旦瀏覽器通過探測請求螟加,以后每次進行 CORS 請求時,就重復(fù)簡單請求步驟(直至探測請求緩存過期)吞琐。
      而如果探測請求通不過(即響應(yīng)沒有任何 CORS 相關(guān)的頭部信息字段)捆探,瀏覽器就知道服務(wù)器會拒絕該 CORS 請求,于是就直接觸發(fā)一個錯誤站粟,回調(diào)給 AJAX 請求的onerror方法黍图。

Spring Boot 配置支持 CORS

一個很幸運的事情就是:瀏覽器會自動幫我們完成 CORS 相關(guān)操作,用戶完全無感知奴烙。
對于開發(fā)者來說助被,前端代碼無需修改剖张,如果是 CORS 請求,瀏覽器會自動幫我們加上相應(yīng)的請求頭進行請求...

因此揩环,實現(xiàn) CORS 通信需要配置的就只是服務(wù)器端搔弄。

下面介紹下在 Spring Boot 中配置 CORS 通信,主要介紹幾種常用的配置方法丰滑,如下所示:

  • @CrossOrigin:該注解可用于方法和類上顾犹,注解在方法上,表示對該方法的請求進行 CORS 校驗褒墨,注解在類上(即Controller上)炫刷,表示該類內(nèi)的方法都遵循該 CORS 校驗。如下所示:

    :前端頁面 AJAX 請求源碼可查看 附錄 內(nèi)容郁妈。

    @Slf4j
    @RestController
    @RequestMapping("cors")
    @CrossOrigin(
            value = "http://127.0.0.1:5500",
            maxAge = 1800,
            allowedHeaders = "*")
    public class CorsController {
    
        @PostMapping("/")
        public String add(@RequestParam("name") String name,
                          @RequestHeader("Origin") String origin) {
            log.info("Request Header ==> Origin: " + origin);
            return "add successfully: " + name;
    
        }
    
        @DeleteMapping("/{id}")
        public String delete(@PathVariable("id") Long id) {
            return String.valueOf(id) + " deleted!";
        }
    }
    

    上述代碼在Controller類上使用@CrossOrigin進行注解配置 CORS浑玛,這樣前端頁面就可以進行 CORS 請求當前Controller下的所有接口。

    其中圃庭,@CrossOrigin注解可選參數(shù)如下:

    方法 作用
    value 表示支持的域锄奢,即Access-Control-Allow-Origin的值
    origins 表示支持的域數(shù)組
    methods 表示支持的 CORS 請求方法,即Access-Control-Allow-Methods的值剧腻。
    其默認值與綁定的控制器方法一致
    maxAge 表示探測請求緩存時間(單位:秒)拘央,即Access-Control-Max-Age的值。
    其默認值為1800书在,也即 30 分鐘
    allowedHeaders 表示允許的請求頭灰伟,即Access-Control-Allow-Headers的值
    默認情況下,支持所有請求頭
    exposedHeaders 表示下發(fā)其他響應(yīng)頭字段給瀏覽器儒旬,即Access-Control-Expose-Headers的值栏账。
    默認不下發(fā)暴露字段
    allowCredentials 表示是否支持瀏覽器發(fā)送認證信息(比如 Cookie),即Access-Control-Allow-Credentials的值栈源。
    默認不支持接收認證信息
  • 全局配置:如果想全局配置 CORS 通信挡爵,只需添加一個配置類。如下所示:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")     //設(shè)置允許跨域的路徑
                    .allowedOrigins("*")
                    .allowedMethods("*")
                    .allowedHeaders("*")
                    .maxAge(1800)
                    .allowCredentials(true);
        }
    }
    

    只需創(chuàng)建一個配置類實現(xiàn)接口WebMvcConfigurer甚垦,然后覆寫方法addCorsMappings即可茶鹃。
    addCorsMappings方法中,registry.addMapping用于設(shè)置可以進行跨域請求的路徑艰亮,比如/cors/**表示路徑/cors/下的所有路由都支持 CORS 請求闭翩。其他的設(shè)置與注解@CrossOrigin一樣,無需介紹迄埃。

    :這里也可以直接通過注入一個WebMvcConfigurer的 Bean 實例疗韵,自定義跨域規(guī)則:

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS");
            }
        };
    }
    
  • 通過Filter配置:通過過濾器Filter可以讓我們手動控制響應(yīng),自然就能完成 CORS 配置侄非。如下所示:

    @Component
    public class CorsFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
    
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    

附錄

  • CORS 前端頁面 AJAX 請求源碼如下所示:

    <html lang="en">
      <!-- ... -->
      <body>
        <button id="cors_post">CORS - POST</button>
        <button id="cors_delete">CORS - DELETE</button>
    
        <script>
          const BASE_URL = 'http://localhost:8080/cors/';
          const postBtn = document.querySelector('#cors_post');
          postBtn.addEventListener('click', async () => {
            // 簡單請求
            const response = await fetch(BASE_URL, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
              },
              body: 'name=Whyn',
            });
            response.text().then((text) => console.log(text));
          });
    
          const delBtn = document.querySelector('#cors_delete');
          delBtn.addEventListener('click', async () => {
            // 非簡單請求
            const response = await fetch(BASE_URL + '1', {
              method: 'DELETE',
            });
            response.text().then((text) => console.log(text));
          });
        </script>
      </body>
    </html>
    

    :前端頁面運行在本地:http://127.0.0.1:5500

  • Spring Security 配置跨域:如果項目中使用了 Spring Security 框架蕉汪,那么也可以直接配置 Spring Security 支持跨域即可:

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 允許跨域資源請求
            // by default uses a Bean by the name of corsConfigurationSource
            http.cors(Customizer.withDefaults());
        }
    
        @Bean
        CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("*"));
            configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS"));
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            // 所有 url 都使用 configuration 定制的跨域規(guī)則
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    }
    

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末流译,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肤无,更是在濱河造成了極大的恐慌先蒋,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宛渐,死亡現(xiàn)場離奇詭異竞漾,居然都是意外死亡,警方通過查閱死者的電腦和手機窥翩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門业岁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寇蚊,你說我怎么就攤上這事笔时。” “怎么了仗岸?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵允耿,是天一觀的道長。 經(jīng)常有香客問我扒怖,道長较锡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任盗痒,我火速辦了婚禮蚂蕴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘俯邓。我一直安慰自己骡楼,他們只是感情好,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布稽鞭。 她就那樣靜靜地躺著鸟整,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朦蕴。 梳的紋絲不亂的頭發(fā)上吃嘿,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音梦重,去河邊找鬼。 笑死亮瓷,一個胖子當著我的面吹牛琴拧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘱支,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚓胸,長吁一口氣:“原來是場噩夢啊……” “哼挣饥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沛膳,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扔枫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后锹安,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體短荐,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年叹哭,在試婚紗的時候發(fā)現(xiàn)自己被綠了忍宋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡风罩,死狀恐怖糠排,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情超升,我是刑警寧澤入宦,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站室琢,受9級特大地震影響乾闰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜研乒,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一汹忠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雹熬,春花似錦宽菜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烈菌,卻和暖如春阵幸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芽世。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工挚赊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人济瓢。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓荠割,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蔑鹦,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354