由于Gravatar官方的域名在中国大陆地区访问并不是很顺畅,所以一般自建的博客都会将地址替换成代理的地址。

例如我在友情链接页面中提到的将Ga­vatar头像URL中 https://secure.gravatar.com/avatar/ 替换为以下任意一项代理地址

https://gravatar.loli.net/avatar/
https://sdn.geekzu.org/avatar/
https://dn-qiniu-avatar.qbox.me/avatar/
https://cravatar.cn/avatar/

这种方法虽然简单,但是可能面临不稳定的问题。因为这些第三方服务会有大量网站使用,或者服务提供者的服务器或者域名到期,都会导致无法访问。之前也有些开发者开发过typecho插件,可以缓存Ga­vatar头像到本地。但是目前为止,基本这类插件在几年前就处于无人维护状态了。

自建一个Gravatar代理

所以还是自建一个Gravatar代理相对靠谱(接下来的方法仅限服务器在海外)。

接下来以Nginx为例,首先在配置文件中添加:

location /avatar/ {
    valid_referers none blocked .ushiromiya.com;
    if ($invalid_referer) {
        return 403;
    }

    proxy_pass https://secure.gravatar.com/avatar/;
    proxy_set_header Host secure.gravatar.com;
}

便能实现指定的域名自动代理secure.gravatar.comavatar目录下的头像文件,并且还能实现防盗链与盗链白名单的效果。在上述示例中,.ushiromiya.com 会匹配 ushiromiya.com 及其所有子域名,从而实现通用的防盗链规则。如果被盗链则返回403状态。

详细解释如下:

  • location /avatar/ { 指定了匹配 URL 路径 /avatar/ 的位置(location)块开始。
  • valid_referers none blocked .ushiromiya.com; 设置防盗链规则。valid_referers 指令定义了允许访问的有效引用者列表。这里的规则是允许没有引用者或者被直接阻止的引用者,以及以 .ushiromiya.com 结尾的引用者。
  • if ($invalid_referer) { 开始一个条件判断块,用于检查引用者是否无效。
  • return 403; 返回403错误码Forbidden,表示防盗链验证失败。
  • proxy_pass https://secure.gravatar.com/avatar/; 将请求代理到目标地址https://secure.gravatar.com/avatar/
  • proxy_set_header Host secure.gravatar.com; 设置代理请求的Host头部为 secure.gravatar.com,以确保请求正确路由到目标服务器。

以上总体实现了对 /avatar/ 路径下的请求进行反向代理,并在防盗链验证失败时返回403错误状态码。

多个防盗链白名单

白名单如果要指定多个域名,则改为:

valid_referers none blocked .ushiromiya.com .example.com;

如果使用了Cloudflare的图片缓存功能,并且由于其他站点引用了反向代理头像而导致403错误页面也被缓存了,这是因为 Cloudflare缓存了错误响应。为了解决这个问题,可以在返回 403 错误时添加Cache-Control指令,指示Cloudflare 不缓存错误响应。可以将以下代码添加到反向代理的配置中:

location /avatar/ {
    valid_referers none blocked .ushiromiya.com;
    if ($invalid_referer) {
        add_header Cache-Control "no-store";
        return 403;
    }

    proxy_pass https://secure.gravatar.com/avatar/;
    proxy_set_header Host secure.gravatar.com;
}

上述配置在返回403错误时添加了Cache-Control头部,其中设置了 "no-store",指示Cloudflare不缓存该响应。

防盗链还可以通过正则表达式匹配检查$http_referer是否满足条件,如果不满足,则执行相应的操作:

if ($http_referer !~* ^https?://([^/]+\.)?ushiromiya\.com(:\d+)?$) {
    add_header Cache-Control "no-store";
    return 403;
}

在这里,使用了一个正则表达式来检查 $http_referer,它匹配不符合以下条件的请求来源:

  • ^https?://:匹配以 http://https:// 开头的字符串
  • ([^/]+\.)?:匹配一个或多个非斜杠字符,后跟一个点号,这部分表示可选的子域名,它允许零个或多个非斜杠字符,以及一个点号
  • ushiromiya\.com:匹配确切的字符串ushiromiya.com 字符串
  • (:\d+)?:匹配冒号后跟一个或多个数字字符的字符串,表示可选的端口号部分,它允许一个冒号和一个或多个数字字符

综合起来,该正则表达式的作用是匹配来自ushiromiya.com及其子域名的请求来源。

因此,如果请求的来源不匹配上述正则表达式,即不是以 https?://([^/]+\.)?ushiromiya.com\.com(:\d+)? 开头的来源,那么就会触发防盗链条件,返回 403 Forbidden 错误。从而满足来源ushiromiya.com及其子域名才被允许访问资源,其他来源将被禁止访问。

只是为了防止文章过长导致视觉疲劳所以插入一张和文章本身没有任何关系的插图
只是为了防止文章过长导致视觉疲劳所以插入一张和文章本身没有任何关系的插图

Nginx缓存头像

除此之外,使用Nginx进行反向代理时,Nginx本身也可以有缓存能力。通过在配置中启用代理缓存,Nginx可以缓存从目标服务器返回的响应,并在后续的请求中直接返回缓存的响应,而无需再次访问目标服务器。

proxy_cache cache_one;
proxy_cache_key $host$uri$is_args$args;
proxy_cache_valid 200 304 301 302 1d;

其中

  • proxy_cache 指定了一个缓存区名称,例如 cache_one
  • proxy_cache_key 指定了缓存键的组成,这里使用了 $host$uri$is_args$args,它包含了请求的主机、URI 以及查询参数。
  • proxy_cache_valid 指定了哪些 HTTP 响应状态码应该被缓存,以及缓存的过期时间。其中状态码 200、304、301、302 都将被缓存,并设置了1天的过期时间。

这些状态码中:

  • 200 正常响应
  • 304 未修改,缓存不变(当客户端发送带有缓存验证的请求时,如果服务器认为客户端的缓存副本仍然有效,服务器会返回 304状态码,表示客户端可以继续使用其缓存副本,而无需重新下载)
  • 302 临时重定向(当服务器希望临时将请求重定向到另一个URL时,可以返回302状态码。客户端在收到302响应后会跟随重定向,并向重定向的URL发送新的请求)
  • 301 永久重定向(当服务器希望永久将请求重定向到另一个URL时,可以返回301状态码。与302类似,客户端在收到301响应后会跟随重定向,并向重定向的URL发送新的请求。不同之处在于,301响应表示该重定向是永久性的,客户端应该将请求的资源URL更新为重定向的URL)

缓存时间也可以是不同单位,s(秒)m(分钟)h(小时)w(周)

这样配置后,Nginx会根据指定的缓存键将响应存储在缓存区中,并在后续的请求中直接返回缓存的响应,而无需再次访问目标服务器,这有助于提高性能并减轻因为大量请求Gravatar服务器的负载。

Nginx缓存大小和空间判断

设置缓存之后,又会有一个问题,当大量不同的Gravatar文件被请求时,可能会导致缓存空间的消耗。为了控制缓存空间的使用,可以用如下的方式限制:

  • 限制单个文件的最大缓存尺寸:可以使用 proxy_cache_max_size 指令来设置单个文件的最大缓存尺寸。如果超过该尺寸,将不会缓存该文件,并将其视为未缓存的请求。例如 proxy_cache_max_size 10m 就是来限制单个文件的最大缓存为10M。
  • 限制所有代理的总缓存量:可以使用 proxy_cache_path 指令中的 max_size 参数来限制整个代理缓存的总容量。该参数指定缓存目录的最大大小。当缓存空间达到该限制时,Nginx会根据缓存策略选择要替换的缓存项。例如 proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=100m 来限制代理缓存的总容量为100M。

其中:

  • proxy_cache_path 指定代理缓存的路径和配置。
  • /path/to/cache 代理缓存的存储路径,根据实际情况替换
  • levels=1:2 可选参数,指定缓存目录的层级结构,缓存目录的层级结构是1级目录和2级目录,这个参数的目的是将缓存文件分散到不同的子目录中,以提高文件查找和性能
  • keys_zone=my_cache:10m 这是一个用于存储缓存键和元数据的内存区域(shared memory),my_cache是缓存区域的名称,根据需要自定义,10m是分配给该缓存区域的内存大小,这个内存区域将用于存储缓存项的键和元数据,以加快缓存查找的速度
  • max_size=100m 这是一个可选的参数,用于指定代理缓存的最大总容量,这里代表代理缓存的最大总容量为100MB

所以以上所有配置合在一起可以写成:

location /avatar/ {
    valid_referers none blocked .ushiromiya.com;  # 设置合法的Referer
    if ($invalid_referer) {
        add_header Cache-Control "no-store";  # 添加Cache-Control头部
        return 403;  # 验证失败返回403错误
    }

    proxy_pass https://secure.gravatar.com/avatar/;  # 反向代理到Gravatar
    proxy_set_header Host secure.gravatar.com;  # 设置代理请求的Host头部

    proxy_cache_valid 200 304 301 302 1d;  # 设置缓存有效期
    proxy_max_temp_file_size 10m;  # 设置临时文件的最大大小

    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=100m;  # 设置代理缓存路径和容量限制
}

注意:这个设置只适用于限制Nginx创建的临时文件的大小,并不会限制代理的文件尺寸。如果源服务器返回的文件大小超过设定的限制,Nginx仍然会接收和代理该文件,只是不会将其写入临时文件进行缓存。

如果需要完全限制代理的文件大小,例如限制源服务器返回的文件大小,就需要使用其他的方法去处理,这里就不展开了。
更可靠的方法,是通过Gravatar参数判断文件基于像素的尺寸,来选择是否拒绝代理,下面我会讲到相关方法。

Gravatar头像等级判断

Gravatar的头像也区分等级,分别分为以下等级:

  • G: suitable for display on all websites with any audience type.
  • PG: may contain rude gestures, provocatively dressed individuals, the lesser swear words, or mild violence.
  • R: may contain such things as harsh profanity, intense violence, nudity, or hard drug use.
  • X: may contain hardcore sexual imagery or extremely disturbing violence.

翻译过来就是

  • G:适合在任何网站上展示给任何类型的受众。 (General Audience)
  • PG:可能包含粗鲁的手势、穿着挑逗性的人、轻微的脏话或轻微的暴力。 (Parental Guidance Suggested)
  • R:可能包含粗俗语言、剧烈的暴力、裸露或硬性毒品使用等内容。 (Restricted)
  • X:可能包含露骨的性描写或极度令人不安的暴力。 (Explicit)

有的时候可能不希望一些不好的头像进行反向代理,影响自己域名,可以选择在代理时对等级参数进行过滤。通常Gravatar的URl为:

https://secure.gravatar.com/avatar/1e4b72c9f00126cbd1c31d4254165d2e?s=200&r=G&d=https://img.ushiromiya.com/Default-Gravatar.jpg

使用正则表达式来匹配$args变量中的等级参数r,如果等级不是G,则返回403 Forbidden错误,并设置Cache-Control头部为no-store,防止被缓存。

if ($args !~* "(^|&)r=G(&|$)") {
    add_header Cache-Control "no-store";
    return 403;
}

如果要同时满足两个等级,如GPG,则写成

if ($args !~* "(^|&)r=(G|PG)(&|$)") {
    add_header Cache-Control "no-store";
    return 403;
}

完整的写法就可以如下:

location /avatar/ {
    valid_referers none blocked .ushiromiya.com;
    if ($invalid_referer) {
        add_header Cache-Control "no-store";
        return 403;
    }

    if ($args !~* "(^|&)r=(G|PG)(&|$)") {
        add_header Cache-Control "no-store";
        return 403;
    }

    proxy_pass https://secure.gravatar.com/avatar/;
    proxy_set_header Host secure.gravatar.com;

    proxy_cache_valid 200 304 301 302 1d;
    proxy_max_temp_file_size 500k;
}

限制可代理图片尺寸(像素)

一般Gravatar有非常多种尺寸,请求哪种尺寸的图片纯粹是靠参数s=数字或者size=数字来实现。如果你输入很大的数字,请求到的图片也会很大。也可以同时限制可代理的最大尺寸。

在这里我就用不能大于600px举例:

if ($arg_s !~* "^(?!0+$)[1-5]?[0-9]{1,2}$|^600$") {
    add_header Cache-Control "no-store";
    return 403;
}

在这段正则中,对于数字1600,不能直接使用[1-600]的形式进行匹配,这样的表达式会被解释为匹配数字160。要匹配数字1600,需要使用更复杂的正则表达式。

目前我用的方法是先通过使用零宽度负预测先行断言(?!0+$)来排除只包含零的情况,然后使用[1-5]?[0-9]{1,2}来匹配1599之间的数字,最后使用^600$匹配600。这样可以确保$s的值在1600之间,而不匹配大于600的数字。如果请求中的ssize参数不符合范围1-600,就会触发条件判断,返回403 Forbidden错误。

零宽度负预测先行断言(zero-width negative lookahead assertion)详细说明:
在正则表达式中,零宽度负预测先行断言是一种特殊的语法结构,用于在匹配内容时向前查看,而不会真正匹配任何字符。它可以用来在匹配之前排除某些模式。

  • (?!0+$): (?!...)表示零宽度负预测先行断言的开始,0+$匹配一个或多个零并以字符串结束,(?!0+$)表示排除只包含零的情况。
  • [1-5]?[0-9]{1,2}:[1-5]?表示数字15出现零次或一次,[0-9]{1,2}表示数字09出现一次或两次。这个部分用于匹配1599之间的数字。
  • 最后使用 |^600$来匹配数字600

同理,如果要表示1-900,则改为:

if ($arg_s !~* "^(?!0+$)[1-9]?[0-9]{1,2}$|^900$") {
    add_header Cache-Control "no-store";
    return 403;
}

先用零宽度负预测先行断言(?!0+$)来排除只包含零的情况。然后使用[1-9]?[0-9]{1,2}来匹配1899之间的数字,以及^900$来匹配数字900

当然也存在更好的正则写法去表述,这里也不详细展开了。

注意:Gravatar支持的最大尺寸为2048px(过去是512px),然而通常用户上传的原始图像尺寸并没这么高。如果强行请求大尺寸文件,只会导致文件无意义变大、图像出现像素化。数据来源于官方文档

重新启动Nginx服务

配置完成后,重新启动Nginx服务以应用新的配置。

sudo service nginx restart

如果支持systemctl,则使用

sudo systemctl restart nginx

替换Typecho所有评论头像地址

参考这篇:《Typecho一些奇奇怪怪但是好用的设置

以上,完工。