这周在项目中遇到一个问题:由于我们前端打包的时候把静态文件的.map文件也上传到了生产环境中,导致这些.map文件可以被访问下载,因此被定性为“有源码泄露的安全风险问题”。因此,需要禁用这些.map文件的访问,于是决定用Nginx添加配置来禁用,但是设置过程中发现怎么都不生效,最后经过了我的各种查找和提问,终于搞清楚了Nginx的配置中location规则的生效规律,最终也解决了问题。

问题背景

项目需求

基于上面提到的问题,这里还有一个限制条件:我们的项目中有个很多Nginx配置,我不能修改默认的配置,只能在默认配置的一个拓展配置中添加新配置,来解决.map文件可访问问题。

我们的这个默认配置关键内容如下:

server {
    ...
    include conf.d/console*.conf.custom;

    location ^~ /itsc-mobile/ {
            proxy_pass http://logic.itsc_mobile/;
        }
    ...
}

其他内容就省略了,其实就是说我不能修改这个默认文件,但是可以增加一个console*.conf.custom文件来添加额外的配置。

我要做的就是让用户无法访问/itsc-mobile/static/js/main.45d35777.js.map这种地址,也就是以.js.map或者.css.map结尾的文件,但是不能影响正常的js和css的访问,比如/itsc-mobile/static/js/main.45d35777.js需要正常访问。

我的做法

我想到的方案是添加一个正则的规则,匹配.js.map或者.css.map结尾的文件就返回403,这样就可以阻止用户访问这种文件了。

于是我在额外配置文件console-ext.conf.custom中添加了如下规则:

location ~* \.(css|js)\.map$ {
    return 403;
}

但是经过验证,发现这个规则根本就不生效,期初以为是我配置的位置不对,后面经过一番验证发现跟配置的位置没关系,就是正则不生效。

于是带着这个问题,我经过了一番搜索和求证,终于搞清楚了Nginx配置里面location的规则的生效规律。

location的语法

这里我根据网上搜到的一些文章,还有官方文档的介绍,自己理解后对location的的语法进行了总结。

这里只讨论下面这种由4个部分组成的语法, 这4个组成部分分别是: location关键字 + 匹配方式符号(可省略)+匹配规则+如何处理, 这个最复杂也是最常用, 我们只讨论这个。

格式大概是这样的:

location [ = | ^~|~ | ~* |  ] uri { ... }

这里的匹配符号有5种:

  1. 无符号:也就是匹配符号为空,算是一种前缀匹配,如 location / {}
  2. =:表示精确匹配,如location = /static/abc.png {}
  3. ^~: 表示优先前缀匹配,如location ^~ /static/js/ {}
  4. ~: 表示区分大小写的正则匹配,如location ~ /static/js/ {}
  5. ~*: 表示不区分大小写的正则匹配,如location ~* /static/js/ {}

location语法的优先级

我这里把这5种规则分成三类,分别是精确匹配、前缀匹配(无符号和优先前缀匹配),正则匹配。

精确匹配

格式如下:

location = /static/abc.png {
        return 403;
    }

精确匹配是要求请求地址跟匹配项完全一致才算匹配成功,所以这种匹配是优先级最高的,只要匹配成功,就不会再进行其他规则的判断,直接返回。

前缀匹配

首先是无符号的前缀匹配:

location /static/js/css/ {
        return 405;
    }

location /static/js/ {
        return 405;
    }

然后是有符号的前缀匹配,这种我称之为优先前缀匹配

location ^~ /static/js/ {
        return 404;
    }

这两个的优先级规律是:命中任何一个前缀匹配的话,还会继续往下去进行规则匹配,并且会从这两个规则中选择匹配到的最长的前缀作为结果,而如果长度相同,那么优先级前缀优先,此时直接返回优先前缀匹配,如果是无符号匹配的更长,则继续去匹配正则规则。

一个例子:/static/js/css/abc 这个地址同时被上面的两个前缀规则匹配,但是很明显无符号的规则可以匹配到/static/js/css/比有符号的匹配前缀/static/js/ 长,所以会选择/static/js/css/最为临时返回规则,进一步去找正则规则。

/static/js/abc 这个地址也同时被上面的规则匹配,但是两个都是匹配到/static/js/,长度一样,此时有符号的优先级就更高,并且不会继续找正则匹配,直接返回。

正则匹配

格式如下:

location ~* \.(css|js)\.map$ {
        return 402;
    }

正则匹配的优先级相对比较低,从上面的前缀匹配也能看到,当无符号的前缀匹配到的情况下才会进行正则匹配,并且如果此时正则匹配到了,那么会取正则的结果,否则取无符号前缀匹配的结果

总结

我经过咨询和学习,对location这里的规则进行了一个总结,并且画了一个流程图来体现:

有如下结论:

  • 首先,不同规则在文件中的顺序对规则的匹配不受影响,只有同类型规则的顺序至上到下进行匹配时是先匹配先生效的
  • 精确匹配优先级最高,只要匹配到就立即返回(最长匹配项),不会进行下一步匹配
  • 前缀匹配中会取匹配到的最长前缀作为预选,当无符号的匹配最长时,还需要进行正则匹配,当优先前缀最长时,直接返回
  • “正则匹配的优先级很低”这个说法其实是不准确的,因为当匹配能到达正则匹配这里的时候,正则匹配的优先级就是最高的,只要匹配就返回,所以真正的问题是匹配会不会到正则匹配这里

一个典型案例

在 Nginx 版本为1.20.1版本条件下,有如下配置文件:

server {
    listen 12080;
    server_name abc.com;

    access_log  /var/log/nginx/test.access.log;
    error_log   /var/log/nginx/test.error.log;

    location ~* \.png$ {
        return 402;
    }

    location / {
        return 400;
    }

    location /static/js/css/ {
        return 405;
    }

    location ^~ /static/ {
        return 401;
    }

    location ^~ /static/js/ {
        return 404;
    }

    location = /static/abc.png {
        return 403;
    }
}

请写出下面的地址的返回码(可以在这个网站上面进行验证 https://nginx.viraptor.info/):

/static/js/css/4.png 这个地址会返回402,因为它同时被无符号前缀和有符号前缀匹配到,但是无符号前缀/static/js/css/更长,此时就继续进行正则匹配,发现可以被正则匹配到,所以返回了正则匹配的状态码402

匹配过程如下:

/static/js/4.png这个地址仅被两个优先前缀匹配,所以直接返回优先前缀匹配最长的/static/js/的404

/static/js/css6/4.png这个地址同时被两个优先级前缀匹配,所以选择最长的返回404

匹配过程如下:

这3个地址的匹配我最开始非常不理解,特别是第1个和第3个的对比,并且把这个疑惑发到了V站进行讨论,也正是这个讨论🔗,让我理解了这里的规律。

学以致用

经过上面的学习,我知道了正则的规则生效的条件,所以我想到了解决项目上问题的方案,之前我配置的正则不生效是因为地址被优先前缀匹配到了,所以直接返回了,根本不会到正则判断这一步,所以为了使正则生效,我需要额外添加一个无符号前缀匹配才行,我的解决方案是这样的:

location ~* \.(css|js)\.map$ {
    return 403;
}

location /itsc-mobile/static/js/ {
    proxy_pass http://logic.itsc_mobile/;
}

我在拓展文件中添加了两个规则,第一个规则自然是正则匹配了,第二个规则是一个无符号前缀匹配规则,这个规则需要比原本的优先级前缀匹配规则长,这样才能走到这里,进而走向正则匹配。

总结

其实在项目里面经常会遇到需要配置Nginx规则的事情,而我们项目里面配置文件又多,规则根本就理不清楚,所以之前配置的时候经常也是不停试探。经过了这次的学习,我总算是理清楚了location的规则,也经过了验证是理解正确的,后续再遇到Nginx配置规则不生效或者产出冲突,就可以按照这个思路去定位解决了。