AI 摘要

本文记录了作者解决Gitea服务被爬虫频繁访问导致服务器负载过高问题的完整过程。关键步骤包括: 1. 通过配置robots.txt阻止合法爬虫访问; 2. 实现Nginx跨域限制,仅允许指定域名访问资源; 3. 设置静态资源防盗链,防止非授权引用; 4. 优化缓存配置,区分动态内容和静态资源的缓存策略。 技术要点: - 使用Nginx的proxy_cache_path配置缓存路径 - 通过valid_referers实现防盗链 - 设置Access-Control-Allow-Origin等头部实现跨域控制 - 对API和admin路径禁用缓存 最终方案显著降低了服务器负载,同时确保了资源的合法访问和安全防护。

引言

起初在我的服务器探针检测到我专门用来部署网站服务的机子经常有异常负载以及带宽。

在关闭全部服务之后逐一排查发现,仅开启gitea docker服务的时候,并没有异常负载,开启除了gitea反代外的服务之后也没有异常负载,但当开启了gitea反代nginx配置文件之后,马上就可以监测到服务器的带宽占用以及时常爆满的cpu利用率。
查看nginx访客日志

如上图所示,有很多bot(爬虫)在爬取我的gitea内容,回顾以前的操作,我在gitea上拉取过一些静态资源(fontawesome之类的),并将仓库属性设置为镜像,还开启了定时拉取。此操作的目的是将自己主网站的资源转到自己的gitea以加快访问速度。但就是这一操作,反复拉取以及设置镜像的过程让那些合法爬虫定位到了我的gitea目录。这些bot大多数都是大公司的,比如openai,Amazon,claude。
不管出于什么目的,我并不希望自己的gitea被反复无效的get占用带宽以及负载资源。因此我便为了解决这个问题做了一些折腾由此还引出了其他方面的折腾,比如跨域限制,防止盗链等等。
果然,生命在于折腾啊(雾
首先解决爬虫问题。
由于这些合法爬虫是遵守君子协议即robots.txt的,所以我们可以配置robots.txt放在网站的根目录。

User-agent: *
Disallow: /

User-agent字段设置规则匹配的bot,*是匹配所有bot。
Disallow字段设置不可访问的目录,/即不可访问所有目录。
但是我反代gitea的nginx配置文件主location反代到了http://localhost:3000,因此是打不开/robots.txt文件的,要放在所有location前的配置文件如下

location = /robots.txt {
    root /path/to/your/domain;  
    try_files $uri =404;
}

好了,引言结束,在完成以上操作后的2-3天,合法爬虫确实不爬取我的网站了,带宽负载均恢复正常。
按理说应该打住结束这次探索了对不对?
可是,生命在于折腾啊!
不继续折腾下去总觉得哪里不完美哪里不完善。
进入正文,为了让我的gitea资源只允许我自己的网站请求资源,需要做跨域限制以及防止盗链处理。

nginx主配置文件

首先修改nginx主配置文件,为了配置区分,在/.../nginx/customize/专门新建一个gitea.conf配置文件

    # 缓存路径配置,路径按自己实际情况填写。
    proxy_cache_path /path/to/your/domain/cache
        levels=1:2 
        keys_zone=GITEA_CACHE:256m 
        inactive=7d 
        max_size=10g 
        use_temp_path=off;

    # 动态缓存控制映射
    map $request_method $skip_cache {
        default     0;
        POST        1;
        PUT         1;
        DELETE      1;
        PATCH       1;
    }

    # CORS配置
    map $http_origin $gitea_allow_origin {
        default "";
        "~^https?://(.*\.)?scarle7\.tech(:[0-9]+)?$" $http_origin;
        "~^https?://(.*\.)?flandre\.vip(:[0-9]+)?$" $http_origin;
        "~^https?://git\.scarle7\.tech(:[0-9]+)?$" $http_origin;
    }

其中keys_zone后面的GITEA_CACHE是定义的变量,gitea_allow_origin也是,后续配置文件要引用它们。
由于上面的配置内容实际是http块里的内容,所以在主配置文件nginx.confhttp块中引用上面的配置文件。

...... #其他内容
http {
    include /.../nginx/customize/gitea.conf
    # 其他原有配置内容不必修改
}

跨域限制

由于上文已经定义了gitea_allow_origin变量,包含了允许的域名。
所以我们可以直接引用它来进行控制

注意:以下代码高亮中的"≠"实际上是"!=",由于代码高亮通过jetbrain默认现代化字体渲染,所以显示效果和jetbrain默认效果一样,此效果称为"字符连写",只是让显示效果更美观并不影响实际复制,你会发现你无法单独复制代码高亮"≠",实际上是两个字符,另外在jetbrain中"<="会渲染成"≤",以此类推有很多特定连写可以自己探索。

        if ($gitea_allow_origin != "") {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

预检请求处理

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

上面内容都是包含在location块里的
完整主location配置应该如下

# 主请求处理
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 跨域控制(引用专用变量)
        if ($gitea_allow_origin != "") {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        # 预检请求处理
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    }

静态资源防盗链

这是一个很简单的事情,通过nginx自带的valid_referersinvalid_referers实现即可。
但是要注意虽然上文已经设置了主location反代,这里依旧要再次设置。

    location ~* \.(css|js|png|jpe?g|gif|ico|svg|woff2|ttf|woff)$ {
    valid_referers none blocked server_names
            scarle7.tech www.scarle7.tech *.scarle7.tech
            flandre.vip www.flandre.vip *.flandre.vip;
        if ($invalid_referer) {
            return 403;
        }
    proxy_pass http://localhost:3000;
}

通过以上设置合法域名访问git.scarle7.tech含有Access-Control-Allow-Origin,可以正常获取,但是访问那些被指定的静态资源却不包含Access-Control-Allow-Origin,因此会被浏览器corb禁止。

原因是上面配置会覆盖掉对于静态资源的corb控制,非上面提及的静态资源仍受主location控制,而被提及的则已经被主location排除。解决方法是在禁止盗链location块也加入corb控制。
禁止盗链完整配置如下

location ~* \.(css|js|png|jpe?g|gif|ico|svg|woff2|ttf|woff)$ {
    valid_referers none blocked server_names
            scarle7.tech www.scarle7.tech *.scarle7.tech
            flandre.vip www.flandre.vip *.flandre.vip;
        if ($invalid_referer) {
            return 403;
        }

        if ($gitea_allow_origin != "") {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    }

缓存控制

由于上文定义的GITEA_CACHE路径并没有创建,先创建路径并授予权限。

sudo mkdir -p /path/to/your/domain/cache
sudo chown -R www:www /path/to/your/domain/cache
sudo chmod -R 755 /path/to/your/domain/cache

分别在上文的两个location中加入

# 主
        proxy_cache GITEA_CACHE;
        proxy_cache_valid 200 302 12h;
        proxy_cache_bypass $skip_cache;
        add_header X-Cache-Status $upstream_cache_status;
# 位置应该在proxy部分和跨域控制部分的中间
# ....proxy部分   cache部分    跨域控制....

# 防盗链
        proxy_cache GITEA_CACHE;
        proxy_cache_valid 200 302 30d;
        expires 365d;
        add_header Cache-Control "public, immutable";
# 位置为proxy_pass前
# ....    cache部分   proxy_pass

对于gitea来说,我们不应该缓存apiadmin,这些页面我们必须实时更新,因此最后一个location专门处理什么内容不缓存

    location ~ ^/(api|admin) {
        proxy_pass http://localhost:3000;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        add_header Cache-Control "no-store";
    }

至此所有设置都完成,重载即可。
最后会给出完整参考配置。
完成这些后,测试gitea是否可以正常访问,然后进行下一步测试。

跨域与盗链完善度验证

1.跨域验证

合法域名测试

curl -I -H "Origin: https://scarle7.tech" https://git.scarle7.tech
# 返回了:Access-Control-Allow-Origin: https://scarle7.tech √验证通过

非法域名测试

例如百度

curl -I -H "Origin: https://baidu.com" https://git.scarle7.tech
# 不包含Access-Control-Allow-Origin头√验证通过

2.防盗链验证

合法引用测试

curl -I -e "https://scarle7.tech" https://git.scarle7.tech/static/image.png
# 返回200 √验证通过

非法引用测试

curl -I -e "https://steal.com" https://git.scarle7.tech/static/image.png
# 返回403 Forbidde √验证通过

3.缓存生效验证

# 首次请求(应MISS)
curl -I https://git.scarle7.tech/test/test.css
# 实际响应头包含:X-Cache-Status: MISS

# 第二次请求(应HIT)
curl -I https://git.scarle7.tech/test/test.css
# 实际响应头包含:X-Cache-Status: HIT
# √验证通过

完整配置

完整配置应该在server块中加上安全头

server
{
    ......
    ......
    # 其他配置,包括监听端口,ssl等等基础配置

    add_header Strict-Transport-Security "max-age=31536000";
    add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"';
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # 君子协议
    location = /robots.txt {
    root /www/wwwroot/git.scarle7.tech;
    try_files $uri =404;
        }

    # 主请求处理
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 缓存配置
        proxy_cache GITEA_CACHE;
        proxy_cache_valid 200 302 12h;
        proxy_cache_bypass $skip_cache;
        add_header X-Cache-Status $upstream_cache_status;

        # 跨域配置
        if ($gitea_allow_origin != "") {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        # 预检请求处理
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }
    }

    # 静态资源防盗链
    location ~* \.(css|js|png|jpe?g|gif|ico|svg|woff2|ttf|woff)$ {
    valid_referers none blocked server_names
            scarle7.tech www.scarle7.tech *.scarle7.tech
            flandre.vip www.flandre.vip *.flandre.vip;
        if ($invalid_referer) {
            return 403;
        }

        if ($gitea_allow_origin != "") {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $gitea_allow_origin always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # 增强缓存
        proxy_cache GITEA_CACHE;
        proxy_cache_valid 200 302 30d;
        expires 365d;
        add_header Cache-Control "public, immutable";
        proxy_pass http://localhost:3000;
    }

    # 不缓存内容
    location ~ ^/(api|admin) {
        proxy_pass http://localhost:3000;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        add_header Cache-Control "no-store";
    }       

    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    access_log  /path/to/logs/access.log;
    error_log  /path/to/logs/error.log;
}

END