Cors跨域(三):Access-Control-Allow-Origin多域名?
前言
你好,我是YourBatman。
本系列前兩篇文章用文字把跨域、Cors相關(guān)概念介紹完了,從下開始進(jìn)入實(shí)戰(zhàn)階段。畢竟學(xué)也學(xué)了,看也看了,是騾子是馬該拉出來(lái)遛一遛。
本文將實(shí)戰(zhàn)Cors解決跨域問題中最為重要的響應(yīng)頭:Access-Control-Allow-Origin。它用于服務(wù)端告訴瀏覽器允許共享本資源的Origin,那么如何允許多個(gè)域名呢?
本文提綱
版本約定
- JDK:8
- Servlet:4.x
- tomcat:9.x
正文
正如前文所述,響應(yīng)頭Access-Control-Allow-Origin 用于在跨域請(qǐng)求中告訴瀏覽器服務(wù)端允許的Origin,瀏覽器拿到這個(gè)頭的值跟自己的Origin對(duì)比決定是否正常接收響應(yīng)。
從命名上就有所察覺:Access-Control-Allow-Origin值是單數(shù),否則就會(huì)叫Access-Control-Allow-Origins?
(瀏覽器)官方對(duì)此響應(yīng)頭的可能值有明確規(guī)定:
也就說此響應(yīng)頭的取值只可能是上圖中的3選1。
null值的作用:讓data:和file:打開的頁(yè)面也能夠共享跨域資源(因?yàn)檫@種協(xié)議下有Origin頭,但是值是null,比較特殊)?
那么問題來(lái)了,倘若服務(wù)端本資源需要允許多個(gè)域來(lái)共享,又該如何指定Access-Control-Allow-Origin 的值呢?這是一個(gè)開發(fā)中常見的場(chǎng)景,本文將繼續(xù)深入討論和介紹最佳實(shí)踐。
環(huán)境準(zhǔn)備
因?yàn)橐獦?gòu)造不同的Origin來(lái)發(fā)送http://localhost:8080/multiple_origins_cors這個(gè)跨域請(qǐng)求,因此需要不同的域名,所以我需要在本機(jī)模擬出來(lái)。我的實(shí)踐方案為:
用本機(jī)Tomcat作為靜態(tài)頁(yè)面服務(wù)器,托管html頁(yè)面
修改本機(jī)host文件,達(dá)到支持多域名的目的
1. Tomcat托管靜態(tài)html頁(yè)面
之前我都是用的IDEA內(nèi)建的靜態(tài)服務(wù)器來(lái)托管html頁(yè)面,但由于它不支持綁定多域名而無(wú)法模擬出本例需要的效果,因此我就不得不開辟新的方法嘍。
做Java開發(fā)的小伙伴對(duì)Tomcat再熟悉不過,但由于Spring Boot的普及它屏蔽了開發(fā)者對(duì)Web Server的感知,所以可能雖然天天用但其實(shí)鮮有接觸,特別是standalone的Tomcat服務(wù)器。
所以我這里稍微介紹下我的做法(關(guān)鍵步驟)。去到Tomcat的目錄,僅需修改它的server.xml文件即可:
步驟一:修改端口為9090(因?yàn)槲襍erver端服務(wù)器也是Tomcat,端口為8080,避免沖突)
步驟二:在host里托管Context上下文,關(guān)聯(lián)到你的html文件夾(Tips:這只是托管的方式之一)
說明:docBase表示靜態(tài)頁(yè)面所在的文件夾(絕對(duì)路徑),path表示對(duì)應(yīng)的url訪問路徑?
完成后,啟動(dòng)tomcat sh startup.sh后即可通過http://localhost:9090/static/xxx.html訪問到靜態(tài)頁(yè)面啦。
2. 修改Host支持多域名
這個(gè)就比較簡(jiǎn)單了,無(wú)需多言,粘張圖就懂。
這樣通過如圖中的3個(gè)域名就都可對(duì)頁(yè)面進(jìn)行正常訪問啦
3. 書寫前端html頁(yè)面
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>多Origin響應(yīng)CORS跨域請(qǐng)求</title>
- <!--導(dǎo)入Jquery-->
- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
- </head>
- <body>
- <button id="btn">多Origin響應(yīng)CORS跨域請(qǐng)求</button>
- <div id="content"></div>
- <script>
- $("#btn").click(function () {
- // 跨域請(qǐng)求
- $.get("http://localhost:8080/multiple_origins_cors", function (result) {
- $("#content").append(result).append("<br/>");
- });
- });
- </script>
- </body>
- </html>
4. 書寫服務(wù)端代碼
- /**
- * 多Origin響應(yīng)
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>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("收到請(qǐng)求:{},方法:{}, Origin頭:{}", requestURI, method, originHeader);
- resp.getWriter().write("hello multiple origins cors...");
- setCrosHeader(resp);
- }
- /**
- * 寫跨域響應(yīng)頭
- */
- private void setCrosHeader(HttpServletResponse resp) {
- resp.setHeader("Access-Control-Allow-Origin", "http://localhost:9090");
- }
- }
至此,環(huán)境已經(jīng)準(zhǔn)備好。此頁(yè)面有三個(gè)地址/域名可以訪問到(不包括localhost),也就是Origin可能有這三種情況:
- http://foo.baidu.com:9090
- http://bar.baidu.com:9090
- http://static.yourbatman.cn:9090
Access-Control-Allow-Origin支持多域名
現(xiàn)實(shí)場(chǎng)景中,服務(wù)端資源如若是完全公開的,那么可以使用Access-Control-Allow-Origin: *。但在現(xiàn)實(shí)場(chǎng)景中大多數(shù)資源并非完全public的,因此需要指定Access-Control-Allow-Origin具體值來(lái)達(dá)到控制的目的。
那么,如何讓Access-Control-Allow-Origin支持多域名呢?下面示范一下常見的錯(cuò)誤方式,最后給出最佳實(shí)踐。
要實(shí)現(xiàn)Access-Control-Allow-Origin允許多個(gè)域名共享資源,按照“常規(guī)思維”,有好些個(gè)使用誤區(qū),這里我嘗試羅列出來(lái)。
誤區(qū)一:Access-Control-Allow-Origin值使用,分隔
,分隔在程序員的世界很常見,很多時(shí)候可表示多值。那在這里是否好使呢?試一試
- private void setCrosHeader(HttpServletResponse resp) {
- resp.setHeader("Access-Control-Allow-Origin", "http://foo.baidu.com:9090,http://bar.baidu.com:9090");
- }
點(diǎn)擊按鈕,發(fā)送跨域請(qǐng)求,失敗詳情:
可以看到不僅沒實(shí)現(xiàn)多值,連foo.baidu.com:9090這個(gè)域名都不能訪問啦~
誤區(qū)二:寫多個(gè)Access-Control-Allow-Origin響應(yīng)頭
這種方式也是“正常思維”之一。試一下:
- 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");
- }
小細(xì)節(jié):這里將setHeader改用為addHeader(xxx)了喲,你懂的?
點(diǎn)擊按鈕,發(fā)送跨域請(qǐng)求,失敗詳情:
多說一句:在實(shí)際開發(fā)中這種出現(xiàn)兩個(gè)Access-Control-Allow-Origin響應(yīng)頭的case還是比較常見的。根據(jù)經(jīng)驗(yàn)一般原因是:Web Server設(shè)置了一個(gè)頭,而Nginx(或者Gateway網(wǎng)關(guān))又添加了一個(gè)頭(一般值為*)。
強(qiáng)調(diào):瀏覽器只要收到兩個(gè)Access-Control-Allow-Origin響應(yīng)頭,不論值是什么(即使一模一樣),都不會(huì)接受。
誤區(qū)三:Access-Control-Allow-Origin值使用正則
當(dāng)需要允許的多域名符合某個(gè)規(guī)律時(shí),會(huì)想到使用簡(jiǎn)單的正則去匹配,那么是否支持呢?試一下:
- private void setCrosHeader(HttpServletResponse resp) {
- resp.addHeader("Access-Control-Allow-Origin", "http://*.baidu.com:9090");
- }
點(diǎn)擊按鈕,發(fā)送跨域請(qǐng)求,失敗詳情:
強(qiáng)調(diào):瀏覽器拿Access-Control-Allow-Origin的值和Origin進(jìn)行匹配的規(guī)則是完全匹配,通配符只認(rèn)*。
誤區(qū)四:Access-Control-Allow-Origin值使用*通配符
這是一個(gè)特殊的使用“誤區(qū)”:它能正常work,但并不能“很好的work”。試一下
- private void setCrosHeader(HttpServletResponse resp) {
- resp.addHeader("Access-Control-Allow-Origin", "*");
- }
點(diǎn)擊按鈕,發(fā)送跨域請(qǐng)求,正常響應(yīng):
既然能夠正常響應(yīng)完成跨域請(qǐng)求,為何我會(huì)認(rèn)為這么處理屬于誤區(qū)呢?
其原因主要為:使用*通配符屬于暴力配置,表示任意源都可以訪問此資源,對(duì)大部分場(chǎng)景來(lái)講這違背了安全原則,存在安全漏洞,所以實(shí)際生產(chǎn)中并不建議這么做(除非是public資源)。
使用*通配符的漏洞
為何對(duì)使用*樂此不疲?答:因?yàn)楹?jiǎn)單,似乎能夠解決“所有”跨域問題,且能一勞永逸。正所謂天下哪有那么多歲月靜好,黑客們?cè)谀谴来烙麆?dòng)。
在與瀏覽器“溝通”過程中,不恰當(dāng)?shù)氖褂肅ors會(huì)造成一些可能的漏洞,比如最常見的便是當(dāng)允許多個(gè)域名跨域請(qǐng)求時(shí),很多同學(xué)為了方便就將Access-Control-Allow-Origin寫為*,或者在Ng上直接賦值為$http_origin(效果完全同*)。這種暴力配置是很危險(xiǎn)的,相當(dāng)于任意網(wǎng)站都可以直接訪問你的資源,那就失去跨域限制的意義了。
這么配置的話,在最基本的滲透測(cè)試中都是過不去的。如若你這么做且公司有安全部門,沒過多久應(yīng)該就會(huì)有人找你聊天喝茶了。
別問我為什么會(huì)知道,因?yàn)槲揖驮话踩块T同事招呼過???
最佳實(shí)踐
來(lái)了,期待的最佳實(shí)踐它來(lái)了。允許多域名跨域是如此常見的場(chǎng)景,本文當(dāng)然要給出最佳實(shí)踐(供以參考)。
既然瀏覽器是精確的完整匹配這個(gè)規(guī)則我們無(wú)法修改,那只有唯一的一個(gè)辦法:在服務(wù)端給Access-Control-Allow-Origin賦值之前做邏輯:
若允許跨域,將請(qǐng)求的Origin賦值給它
若不允許跨域,不返回此頭(或者給賦值一個(gè)默認(rèn)值也是可以的)
有了理論支撐,用代碼實(shí)現(xiàn)乃分分鐘之事:
- 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,可以這么寫(簡(jiǎn)單舉例而已):
- 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賦值邏輯。腦洞更大一點(diǎn),這可極具個(gè)性化和擴(kuò)展性:
- ALLOW_ORIGINS:不需要再hard code,可以支持外部化配置,甚至打通配置中心
- 匹配算法:可以支持完全匹配、前綴匹配、正則匹配,設(shè)置更復(fù)雜的匹配邏輯都可
- ...
說了這么多,這些個(gè)性化擴(kuò)展性都需要代碼去實(shí)現(xiàn),那到底有沒有現(xiàn)成可用的最佳實(shí)踐代碼呢?
當(dāng)然,有!!!
作為Java開發(fā)者yyds:Spring框架。怎能沒考慮到這么常見的Cors跨域場(chǎng)景呢?它提供的org.springframework.web.filter.CorsFilter就是真實(shí)可用的最佳實(shí)踐,可以拿來(lái)就用或者作為參考和學(xué)習(xí)。
說明:關(guān)于Spring/Spring Boot場(chǎng)景下對(duì)Cors跨域問題的解決方案以及原理分析,本系列已安排在下下篇詳細(xì)剖析?
補(bǔ)充:Vary: Origin解決緩存問題
在文章最后想補(bǔ)充一個(gè)“小知識(shí)點(diǎn)”:有關(guān)于瀏覽器緩存和Vary的問題。
關(guān)于Vary,平時(shí)比較細(xì)心的同學(xué)應(yīng)該會(huì)比較有印象。Vary中文含義:變化。它是一個(gè)HTTP響應(yīng)頭,決定了對(duì)于下一個(gè)請(qǐng)求,應(yīng)該使用緩存還是向源服務(wù)器請(qǐng)求一個(gè)新的Response,和內(nèi)容協(xié)商(你知道的,內(nèi)容協(xié)商也屬于我的一個(gè)技術(shù)專欄)有關(guān)?,F(xiàn)在的瀏覽器都支持這個(gè)響應(yīng)頭~
標(biāo)準(zhǔn)語(yǔ)法是:
- ary: * // 告訴瀏覽器,所有的響應(yīng)頭都是變得所以都不緩存
- Vary: <header-name>, <header-name>, ... // 告訴瀏覽器,有些頭都是變的就不要緩存了
說了這么多,它和本文有何關(guān)系呢?
由于這和瀏覽器緩存(cache-control)背景知識(shí)強(qiáng)關(guān)聯(lián),并非本文重點(diǎn)無(wú)需詳細(xì)展開。因此這里只是提示你:如若出現(xiàn)同一份URL(相同的Referer),不同的Origin(如foo.baidu.com和bar.baidu.com)請(qǐng)求時(shí)一個(gè)能行一個(gè)不能行,那很有可能就是瀏覽器緩存導(dǎo)致,這時(shí)就可以增加一個(gè)響應(yīng)頭Vary: Origin來(lái)解決。
?說明:這里假設(shè)服務(wù)端對(duì)Access-Control-Allow-Origin的賦值邏輯一切正常,也就是說服務(wù)端沒有問題?
總結(jié)
本文圍繞Access-Control-Allow-Origin這個(gè)響應(yīng)頭,從幾大誤區(qū)到最佳實(shí)踐,希望能夠幫助你加深對(duì)它的理解。當(dāng)然最重要的是:盡量不要一碰到Access-Control-Allow-Origin就只會(huì)賦值*啦,多些思考多些安全性考慮,畢竟安全部門的茶水最好還是不要喝。