专栏首页BAT的乌托邦Cors跨域(三):Access-Control-Allow-Origin多域名?

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

先把数据结构搞清楚,程序的其余部分自现。 本文已被https://yourbatman.cn收录;公号后台回复“专栏列表”获取全部小而美的原创技术专栏

前言

你好,我是YourBatman

本系列前两篇文章用文字把跨域、Cors相关概念介绍完了,从下开始进入实战阶段。毕竟学也学了,看也看了,是骡子是马该拉出来遛一遛。

本文将实战Cors解决跨域问题中最为重要的响应头:Access-Control-Allow-Origin。它用于服务端告诉浏览器允许共享本资源的Origin,那么如何允许多个域名呢?

所属专栏

本文提纲

版本约定

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

正文

正如前文所述,响应头Access-Control-Allow-Origin 用于在跨域请求中告诉浏览器服务端允许的Origin,浏览器拿到这个头的值跟自己的Origin对比决定是否正常接收响应。

从命名上就有所察觉:Access-Control-Allow-Origin值是单数,否则就会叫Access-Control-Allow-Origins

(浏览器)官方对此响应头的可能值有明确规定:

也就说此响应头的取值只可能是上图中的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,避免冲突)

步骤二:在host里托管Context上下文,关联到你的html文件夹(Tips:这只是托管的方式之一)

说明:docBase表示静态页面所在的文件夹(绝对路径),path表示对应的url访问路径

完成后,启动tomcat sh startup.sh后即可通过http://localhost:9090/static/xxx.html访问到静态页面啦。

2. 修改Host支持多域名

这个就比较简单了,无需多言,粘张图就懂。

这样通过如图中的3个域名就都可对页面进行正常访问啦

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: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("收到请求:{},方法:{}, 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");
}

点击按钮,发送跨域请求,失败详情:

可以看到不仅没实现多值,连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)了哟,你懂的

点击按钮,发送跨域请求,失败详情:

多说一句:在实际开发中这种出现两个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");
}

点击按钮,发送跨域请求,失败详情:

强调:浏览器拿Access-Control-Allow-Origin的值和Origin进行匹配的规则是完全匹配,通配符只认*

误区四:Access-Control-Allow-Origin值使用*通配符

这是一个特殊的使用“误区”:它能正常work,但并不能“很好的work”。试一下

private void setCrosHeader(HttpServletResponse resp) {
    resp.addHeader("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就只会赋值*啦,多些思考多些安全性考虑,毕竟安全部门的茶水最好还是不要喝。

本文思考题

本文已被https://yourbatman.cn收录。公号后台回复专栏列表即可进入专栏详情。看完了不一定懂,看懂了不一定会。来,3个思考题帮你复盘:

  1. Access-Control-Allow-Origin可以设置多个头吗?
  2. 如何让多个域名都可以访问到本地的Html文件?
  3. 在Spring Framework场景下,解决跨域问题的最佳方案是什么?

推荐阅读

System.out.println("点个赞吧");
echo('关注【BAT的乌托邦】');
console.log("私聊YourBatman:fsx1056342982");

我是YourBatman:一个早在2013年就已毕业的大龄程序员。网瘾失足、清考、延期毕业、房产中介、送外卖、销售…是我不可抹灭的标签。

  • 2013.08-2014.07在宁夏银川中介公司卖二手房1年,毕业后第1份工作
  • 2014.07-2015.05在荆州/武汉/北京,从事炸鸡排、卖保险、直销以及送外卖工作,这是第2,3,4,5份工作
  • 2015.08从事Java开发,闯过外包,呆过大厂!Java架构师、博客专家,Spring开源贡献者。喜欢写代码,有代码洁癖;重视基础,坚信底层基础决定上层建筑
  • 现致力于写纯粹技术专栏,虽很难,但走自己的路不哗众取宠。如果你也有共鸣也是码农,可加我好友一起交流学习(备注:java)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 跨域问题(CORS / Access-Control-Allow-Origin)

    最近在项目中,调用Eureka REST接口时,出现了CORS跨越问题(Cross-origin resource sharing),在此与大家进行分享,避免多...

    xcbeyond
  • 浏览器中的跨域问题与 CORS

    跨域,这或许是前端面试中最常碰到的问题了,大概因为跨域问题是浏览器环境中的特有问题,而且随处可见,如同蚊子不仅盯你肉而且处处围着你转让你心烦。「你看,在服务器发...

    山月
  • 浏览器中的跨域问题与 CORS

    跨域,这或许是前端面试中最常碰到的问题了,大概因为跨域问题是浏览器环境中的特有问题,而且随处可见,如同蚊子不仅盯你肉而且处处围着你转让你心烦。「你看,在服务器发...

    夜尽天明
  • Yii支持多域名cors原理的实现

    平常我们遇到跨域问题时,常使用 cors(Cross-origin resource sharin)方式解决。不知你是否注意到,在设置响应头 Access-Co...

    砸漏
  • 跨域共享CORS详解及Gin配置跨域

    常见_youmen
  • Web Security 之 CORS

    在本节中,我们将解释什么是跨域资源共享(CORS),并描述一些基于 CORS 的常见攻击示例,以及讨论如何防御这些攻击。

    凌虚
  • 014.Nginx跨域配置

    同源策略是一个安全策略。同源,指的是协议,域名,端口相同。浏览器处于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读...

    木二
  • CORS攻击原理介绍和使用

    注意:本文分享给安全从业人员,网站开发人员和运维人员在日常工作中使用和防范恶意攻击,请勿恶意使用下面描述技术进行非法操作。

    WeiyiGeek
  • Web漏洞 | CORS跨域资源共享漏洞

    有关于浏览器的同源策略和如何跨域获取资源,传送门 -->浏览器同源策略和跨域的实现方法

    Gcow安全团队
  • 如何配置ajax请求跨域携带cookie,cors支持ajax请求携带cookie

    3、ajax在发送跨域请求时如果想携带cookie,必须将请求对象的withcredentials属性设置为true。

    挥刀北上
  • 跨域资源共享 CORS 详解

    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpR...

    ruanyf
  • CORS原理及@koa/cors源码解析

    这是浏览器的同源策略所造成的,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

    木子星兮
  • 跨域资源CORS简介

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

    javascript.shop
  • 跨域资源共享(CORS)是什么?

    出于安全原因,浏览器会限制脚本发起的跨域 HTTP 请求,除非服务器同意访问。譬如服务器对预检请求的响应 Header 中有 Access-Control-Al...

    Learn-anything.cn
  • 对象存储COS跨域CORS问题小结

    CORS(Cross-origin resource sharing) 中文名称"跨域资源共享",由于安全原因,Web 应用程序默认情况只能在同源(协议、域名和...

    吴硕卫
  • 商城项目-跨域问题

    而我们刚才是从manage.leyou.com去访问api.leyou.com,这属于二级域名不同,跨域了。

    cwl_java
  • GIN框架解决跨域问题

    当两个域具有相同的协议(如http), 相同的端口(如80),相同的host,那么我们就可以认为它们是相同的域(协议,域名,端口都必须相同)。

    秦穆之
  • 系统服务化构建-跨域CROS

    文本讨论关于接口开发中的跨域 CORS。CORS 是一种浏览器协议,源于 HTTP 请求的安全策略,在这个体系中的关键词有,同源策略,XMLHttpReques...

    needrunning
  • CORS解决跨域问题

    浏览器中,网站A的网络请求访问网站A的资源(图片,HTTP请求)是很顺畅的,而想访问网站B的资源,就要面对跨域资源访问的问题了。面对跨域问题,有很多的解决方案,...

    zhangyunfeiVir

扫码关注云+社区

领取腾讯云代金券