天天看點

Cors跨域(三):Access-Control-Allow-Origin多域名?

先把資料結構搞清楚,程式的其餘部分自現。

前言

你好,我是YourBatman。

本系列前兩篇文章用文字把跨域、Cors相關概念介紹完了,從下開始進入實戰階段。畢竟學也學了,看也看了,是騾子是馬該拉出來遛一遛。

本文将實戰Cors解決跨域問題中最為重要的響應頭:Access-Control-Allow-Origin。它用于服務端告訴浏覽器允許共享本資源的Origin,那麼如何允許多個域名呢?

所屬專欄

  • 點撥-Cors跨域

本文提綱

Cors跨域(三):Access-Control-Allow-Origin多域名?

版本約定

  • JDK:8
  • Servlet:4.x
  • tomcat:9.x

正文

正如前文所述,響應頭Access-Control-Allow-Origin 用于在跨域請求中告訴浏覽器服務端允許的Origin,浏覽器拿到這個頭的值跟自己的Origin對比決定是否正常接收響應。

從命名上就有所察覺:Access-Control-Allow-Origin值是單數,否則就會叫Access-Control-Allow-Origins

(浏覽器)官方對此響應頭的可能值有明确規定:

Cors跨域(三):Access-Control-Allow-Origin多域名?

也就說此響應頭的取值隻可能是上圖中的3選1。

null值的作用:讓data:和file:打開的頁面也能夠共享跨域資源(因為這種協定下有Origin頭,但是值是null,比較特殊)

那麼問題來了,倘若服務端本資源需要允許多個域來共享,又該如何指定Access-Control-Allow-Origin 的值呢?這是一個開發中常見的場景,本文将繼續深入讨論和介紹最佳實踐。

環境準備

因為要構造不同的Origin來發送http://localhost:8080/multiple_origins_cors這個跨域請求,是以需要不同的域名,是以我需要在本機模拟出來。我的實踐方案為:

  • 用本機Tomcat作為靜态頁面伺服器,托管html頁面
  • 修改本機host檔案,達到支援多域名的目的

1. Tomcat托管靜态html頁面

之前我都是用的IDEA内建的靜态伺服器來托管html頁面,但由于它不支援綁定多域名而無法模拟出本例需要的效果,是以我就不得不開辟新的方法喽。

做Java開發的小夥伴對Tomcat再熟悉不過,但由于Spring Boot的普及它屏蔽了開發者對Web Server的感覺,是以可能雖然天天用但其實鮮有接觸,特别是standalone的Tomcat伺服器。

是以我這裡稍微介紹下我的做法(關鍵步驟)。去到Tomcat的目錄,僅需修改它的server.xml檔案即可:

步驟一:修改端口為9090(因為我Server端伺服器也是Tomcat,端口為8080,避免沖突)

Cors跨域(三):Access-Control-Allow-Origin多域名?

步驟二:在host裡托管Context上下文,關聯到你的html檔案夾(Tips:這隻是托管的方式之一)

Cors跨域(三):Access-Control-Allow-Origin多域名?
說明:docBase表示靜态頁面所在的檔案夾(絕對路徑),path表示對應的url通路路徑

完成後,啟動tomcat sh startup.sh後即可通過http://localhost:9090/static/xxx.html通路到靜态頁面啦。

Cors跨域(三):Access-Control-Allow-Origin多域名?

2. 修改Host支援多域名

這個就比較簡單了,無需多言,粘張圖就懂。

Cors跨域(三):Access-Control-Allow-Origin多域名?

這樣通過如圖中的3個域名就都可對頁面進行正常通路啦

Cors跨域(三):Access-Control-Allow-Origin多域名?
Cors跨域(三):Access-Control-Allow-Origin多域名?

3. 書寫前端html頁面

multiple_origins_cors.html内容如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>多Origin響應CORS跨域請求</title>
    <!--導入Jquery-->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
</head>
<body>
<button id="btn">多Origin響應CORS跨域請求</button>
<div id="content"></div>

<script>
    $("#btn").click(function () {
        // 跨域請求
        $.get("http://localhost:8080/multiple_origins_cors", function (result) {
            $("#content").append(result).append("<br/>");
        });
    });
</script>
</body>
</html>
      

4. 書寫服務端代碼

/**
 * 多Origin響應
 *
 * @author YourBatman. <a href=mailto:[email protected]>Send email to me</a>
 * @site https://yourbatman.cn
 * @date 2021/6/9 10:36
 * @since 0.0.1
 */
@Slf4j
@WebServlet(urlPatterns = "/multiple_origins_cors")
public class MultipleOriginsCorsServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();
        String method = req.getMethod();
        String originHeader = req.getHeader("Origin");
        log.info("收到請求:{},方法:{}, Origin頭:{}", requestURI, method, originHeader);

        resp.getWriter().write("hello multiple origins cors...");
        setCrosHeader(resp);
    }

    /**
     * 寫跨域響應頭
     */
    private void setCrosHeader(HttpServletResponse resp) {
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:9090");
    }
}
      

至此,環境已經準備好。此頁面有三個位址/域名可以通路到(不包括localhost),也就是Origin可能有這三種情況:

  1. http://foo.baidu.com:9090
  2. http://bar.baidu.com:9090
  3. http://static.yourbatman.cn:9090

Access-Control-Allow-Origin支援多域名

現實場景中,服務端資源如若是完全公開的,那麼可以使用Access-Control-Allow-Origin: *。但在現實場景中大多數資源并非完全public的,是以需要指定Access-Control-Allow-Origin具體值來達到控制的目的。

那麼,如何讓Access-Control-Allow-Origin支援多域名呢?下面示範一下常見的錯誤方式,最後給出最佳實踐。

要實作Access-Control-Allow-Origin允許多個域名共享資源,按照“正常思維”,有好些個使用誤區,這裡我嘗試羅列出來。

誤區一:Access-Control-Allow-Origin值使用,分隔

,分隔在程式員的世界很常見,很多時候可表示多值。那在這裡是否好使呢?試一試

private void setCrosHeader(HttpServletResponse resp) {
    resp.setHeader("Access-Control-Allow-Origin", "http://foo.baidu.com:9090,http://bar.baidu.com:9090");
}
      

點選按鈕,發送跨域請求,失敗詳情:

Cors跨域(三):Access-Control-Allow-Origin多域名?
Cors跨域(三):Access-Control-Allow-Origin多域名?

可以看到不僅沒實作多值,連foo.baidu.com:9090這個域名都不能通路啦~

誤區二:寫多個Access-Control-Allow-Origin響應頭

這種方式也是“正常思維”之一。試一下:

private void setCrosHeader(HttpServletResponse resp) {
    resp.addHeader("Access-Control-Allow-Origin", "http://foo.baidu.com:9090");
    resp.addHeader("Access-Control-Allow-Origin", "http://bar.baidu.com:9090");
}
      
小細節:這裡将setHeader改用為addHeader(xxx)了喲,你懂的
Cors跨域(三):Access-Control-Allow-Origin多域名?
Cors跨域(三):Access-Control-Allow-Origin多域名?

多說一句:在實際開發中這種出現兩個Access-Control-Allow-Origin響應頭的case還是比較常見的。根據經驗一般原因是:Web Server設定了一個頭,而Nginx(或者Gateway網關)又添加了一個頭(一般值為*)。

強調:浏覽器隻要收到兩個Access-Control-Allow-Origin響應頭,不論值是什麼(即使一模一樣),都不會接受。

誤區三:Access-Control-Allow-Origin值使用正則

當需要允許的多域名符合某個規律時,會想到使用簡單的正則去比對,那麼是否支援呢?試一下:

private void setCrosHeader(HttpServletResponse resp) {
    resp.addHeader("Access-Control-Allow-Origin", "http://*.baidu.com:9090");
}
      
Cors跨域(三):Access-Control-Allow-Origin多域名?
Cors跨域(三):Access-Control-Allow-Origin多域名?

強調:浏覽器拿Access-Control-Allow-Origin的值和Origin進行比對的規則是完全比對,通配符隻認*。

誤區四:Access-Control-Allow-Origin值使用*通配符

這是一個特殊的使用“誤區”:它能正常work,但并不能“很好的work”。試一下

private void setCrosHeader(HttpServletResponse resp) {
    resp.addHeader("Access-Control-Allow-Origin", "*");
}
      

點選按鈕,發送跨域請求,正常響應:

Cors跨域(三):Access-Control-Allow-Origin多域名?
Cors跨域(三):Access-Control-Allow-Origin多域名?

既然能夠正常響應完成跨域請求,為何我會認為這麼處理屬于誤區呢?

其原因主要為:使用*通配符屬于暴力配置,表示任意源都可以通路此資源,對大部分場景來講這違背了安全原則,存在安全漏洞,是以實際生産中并不建議這麼做(除非是public資源)。

使用*通配符的漏洞

為何對使用*樂此不疲?答:因為簡單,似乎能夠解決“所有”跨域問題,且能一勞永逸。正所謂天下哪有那麼多歲月靜好,黑客們在那蠢蠢欲動。

在與浏覽器“溝通”過程中,不恰當的使用Cors會造成一些可能的漏洞,比如最常見的便是當允許多個域名跨域請求時,很多同學為了友善就将Access-Control-Allow-Origin寫為*,或者在Ng上直接指派為$http_origin(效果完全同*)。這種暴力配置是很危險的,相當于任意網站都可以直接通路你的資源,那就失去跨域限制的意義了。

這麼配置的話,在最基本的滲透測試中都是過不去的。如若你這麼做且公司有安全部門,沒過多久應該就會有人找你聊天喝茶了。

别問我為什麼會知道,因為我就曾被安全部門同僚招呼過????

最佳實踐

來了,期待的最佳實踐它來了。允許多域名跨域是如此常見的場景,本文當然要給出最佳實踐(供以參考)。

既然浏覽器是精确的完整比對這個規則我們無法修改,那隻有唯一的一個辦法:在服務端給Access-Control-Allow-Origin指派之前做邏輯:

  • 若允許跨域,将請求的Origin指派給它
  • 若不允許跨域,不傳回此頭(或者給指派一個預設值也是可以的)

有了理論支撐,用代碼實作乃分分鐘之事:

private List<String> ALLOW_ORIGINS = new ArrayList<>();
@Override
public void init() throws ServletException {
    ALLOW_ORIGINS.add("http://localhost:9090");
    ALLOW_ORIGINS.add("http://foo.baidu.com:9090");
    ALLOW_ORIGINS.add("http://bar.baidu.com:9090");
    ALLOW_ORIGINS.add("http://static.yourbatman.cn:9090");
}

private void setCrosHeader(String reqOrigin, HttpServletResponse resp) {
    if (reqOrigin == null) {
        return;
    }
    // 比對算法:equals
    if (ALLOW_ORIGINS.contains(reqOrigin)) {
        resp.addHeader("Access-Control-Allow-Origin", reqOrigin);
    }
}
      

如果是Ng,可以這麼寫(簡單舉例而已):

location / {  
	
	// 枚舉列出允許跨域的domian(可以使用NG支援的比對方式)
	set $cors_origin "";
    if ($http_origin ~* "^http://foo.baidu.com$") {
            set $cors_origin $http_origin;
    }
    if ($http_origin ~* "^http://bar.baidu.com$") {
            set $cors_origin $http_origin;
    }
    add_header Access-Control-Allow-Origin $cors_origin;
}
      

既然接管了Access-Control-Allow-Origin指派邏輯。腦洞更大一點,這可極具個性化和擴充性:

  • ALLOW_ORIGINS:不需要再hard code,可以支援外部化配置,甚至打通配置中心
  • 比對算法:可以支援完全比對、字首比對、正則比對,設定更複雜的比對邏輯都可

說了這麼多,這些個性化擴充性都需要代碼去實作,那到底有沒有現成可用的最佳實踐代碼呢?

當然,有!!!

作為Java開發者yyds:Spring架構。怎能沒考慮到這麼常見的Cors跨域場景呢?它提供的org.springframework.web.filter.CorsFilter就是真實可用的最佳實踐,可以拿來就用或者作為參考和學習。

說明:關于Spring/Spring Boot場景下對Cors跨域問題的解決方案以及原理分析,本系列已安排在下下篇詳細剖析

補充:Vary: Origin解決緩存問題

在文章最後想補充一個“小知識點”:有關于浏覽器緩存和Vary的問題。

關于Vary,平時比較細心的同學應該會比較有印象。Vary中文含義:變化。它是一個HTTP響應頭,決定了對于下一個請求,應該使用緩存還是向源伺服器請求一個新的Response,和内容協商(你知道的,内容協商也屬于我的一個技術專欄)有關。現在的浏覽器都支援這個響應頭~

标準文法是:

Vary: * // 告訴浏覽器,所有的響應頭都是變得是以都不緩存
Vary: <header-name>, <header-name>, ... // 告訴浏覽器,有些頭都是變的就不要緩存了
      

說了這麼多,它和本文有何關系呢?

由于這和浏覽器緩存(cache-control)背景知識強關聯,并非本文重點無需詳細展開。是以這裡隻是提示你:如若出現同一份URL(相同的Referer),不同的Origin(如foo.baidu.com和bar.baidu.com)請求時一個能行一個不能行,那很有可能就是浏覽器緩存導緻,這時就可以增加一個響應頭Vary: Origin來解決。

說明:這裡假設服務端對Access-Control-Allow-Origin的指派邏輯一切正常,也就是說服務端沒有問題

總結

本文圍繞Access-Control-Allow-Origin這個響應頭,從幾大誤區到最佳實踐,希望能夠幫助你加深對它的了解。當然最重要的是:盡量不要一碰到Access-Control-Allow-Origin就隻會指派*啦,多些思考多些安全性考慮,畢竟安全部門的茶水最好還是不要喝。

本文思考題

  1. Access-Control-Allow-Origin可以設定多個頭嗎?
  2. 如何讓多個域名都可以通路到本地的Html檔案?
  3. 在Spring Framework場景下,解決跨域問題的最佳方案是什麼?

Cors跨域(三):Access-Control-Allow-Origin多域名?

System.out.println("點個贊吧");
echo('關注【BAT的烏托邦】');
console.log("私聊YourBatman:fsx1056342982");
      
  • 2013.08-2014.07甯夏銀川中介公司賣二手房1年,畢業後第1份工作
  • 2014.07-2015.05荊州/武漢/北京,從事炸雞排、賣保險、直銷、送外賣工作,這是第2,3,4,5份工作
  • 2015.08開始從事Java開發,做過兼職,闖過外包,呆過大廠!現為我司基礎架構團隊負責人。Java架構師、Spring開源貢獻者,部落格專家,領域模組化專家。熱衷寫代碼,有代碼潔癖;重視基礎和基建,相信效率為王

繼續閱讀