前两天在 贤民的比特记忆 看到这篇文章:为 hugo 站点插入豆瓣条目的 shortcode,我也学着把豆瓣条目的 shortcode 加上了。之后发现一个小问题,在自己的页面上引用了豆瓣上电影海报或书籍封面的图片地址,有时会触发豆瓣的防盗链措施,结果就是对图片的请求都是 403,没有访问权限。

一般的防盗链就是根据 HTTP 请求 Header 里的 Referrer 字段的值来判断是正常应答,还是返回 403。针对这种防盗链措施,一般又两种反防盗链措施,一是发起 HTTP 请求时不要在 Header 里带 Referrer 字段,第二种是用反向代理

有些 Web 服务的防盗链措施把不带 Referrer 的请求也当作非法的请求返回 403。如果你想尝试让浏览器在发起请求时修改 Referrer,不可行,浏览器不允许这样做,也就是说在浏览器里的请求要么带着默认的 Referrer,要么没有 Referrer。这样一来,第一条路就被堵死。还好豆瓣图片服务器并没有这么做,对 Referrer 为空的 HTTP 请求,是放行的,跟大多数 Web 服务一样。

Referrer

主流浏览器图片反防盗链方法总结 这篇文章里对第一种反防盗链措施有详细的总结,有两种方法可以实现。

  • 给页面添加 meta 标签。``
  • 给页面元素添加 Referrer 属性。xxx

前一种方法是页面级的修改,针对这个页面里任何资源的请求,都不带 Referer 了。第二种方法只针对指定元素的请求不带 Referer。两种方法在不同浏览器中的兼容性都多多少少有点问题,不过这到不在我的考虑范围——用旧版 IE 的人也不会访问到这里来——所以我本打算用第二种方法,给豆瓣图片的链接添加 Referrer 属性。

结果我一看,这个豆瓣 shortcode 的实现里,图片这部分的代码是类似这样的:

并不是我以为的

给前面那一行代码里的 div 标签添加 ref 属性是没有用的。所以,要么改成下面 img 标签的形式,同时也要修改对应的 CSS,保证内容的正常展示;要么就用反防盗链的第二种措施,反向代理。

反向代理

跟第一种措施比起来,做反向代理有点兴师动众的意味。即便是跟修改 HTML 和 CSS 代码比起来,做反向代理也是跟重量级的操作。

你至少要有自己的域名和 VPS 或者云主机才行,你要配置 Web Server 作为反向代理的工具,而且最后你还是要修改原来那段 shortcode 的代码,把豆瓣图片 URL 中的域名替换为自己反向代理的域名。

现在想一下,我也不明白为什么我选了这个方案,可能这种做法兼容性好,比较“万能”吧。更为可能的原因,可能是我想折腾折腾 Nginx 了。

Nginx 配置

Nginx 做反向代理的核心配置是这么几行,假设要把 img1.example.com 作为 img1.doubanio.com 的反向代理域名:

server {
    listen 80;
    server_name example.com
    location / {
        proxy_pass https://img1.doubanio.com;
    }
}

豆瓣的图片资源在 img*.doubanio.com 域名上,据我所知,目前有 img1.doubanio.com, img3.doubanio.com 两个子域名,也可能有其他的子域名。最好用 Nginx 的正则表达式功能,我准备把 *-douban.ifttl.com 这种格式的域名作为 *.doubanio.com 的反向代理域名。

下面是我实际在用的 Nginx 配置。

server {
    listen 80;
    listen [::]:80;
    server_name "~^(?.*)\-douban\.ifttl\.com$";
    valid_referers *.ifttl.com ifttl.com;
    if ($invalid_referer) {
        return 403;
    }
    location / {
        proxy_pass https://$name.doubanio.com;
        proxy_redirect off;
        proxy_set_header Referer "https://www.douban.com";
    }
}

关键是 server_name "~^(?.*)\-douban\.ifttl\.com$"; 这一行正则表达式指定的主机名,并且把开头部分捕获到 name 变量里,交给 proxy_pass 参数 https://$name.doubanio.com

这个应用场景里没有重定向和修改重定向地址的需求,所以 proxy_redirect off

proxy_set_header Referer "https://www.douban.com"; 这一行也比较重要,添加这样一行配置,在 Nginx 把请求转向豆瓣服务器时,在 HTTP 请求头里加了个 Referrer 是 https://www.douban.com,也就是好像是从 https://www.douban.com 页面中发出的请求,让豆瓣的图片服务器当作是自家人的请求。这样感觉我对豆瓣的良心大大的坏。

上面配置里这一段内容就体现了我对豆瓣的真良心。

valid_referers *.ifttl.com ifttl.com;
if ($invalid_referer) {
    return 403;
}

这是我对做反向代理域名做的在自己这一侧的防盗链,虽然被盗链的概率很小,而且我也不怎么在乎被盗链,但是既然最终请求——没有缓存的——都要落到豆瓣服务器上,我还是挡一下吧。这几行配置设置了有效的 Referrer 为 ifttl.com 和它的所有子域名,如果请求到我这来,但是 Referrer 值不在其中,我也只响应 403,不再处理。

一个插曲

Nginx reload,尝试请求自己的服务器,结果收获一个 502 错误,Nginx 错误日志里记录着:

no resolver defined to resolve img3.doubanio.com

原因很明确,没有指定 resolver,无法解析 proxy_pass 指定的域名的地址,Nginx 不知道该往哪里转发请求。给 Nginx 添加 resolver 就可以解决问题了,需要注意的是,resolver 必须配置在 http 块,不能配置到 server 块。

http {
    # ...
    # ...
        # 可以配置多个地址作为 resolver
    resolver 8.8.8.8 208.67.222.222
    # ...
    # server {
    #     ...
    # }
}

这时我产生了一个疑问,当 proxy_pass 的值是个固定的域名时,不需要配置 resolver,而 proxy_pass 的值里面包含变量时,就要配置 resolver。明明都需要 DNS 解析,为什么是固定域名时不需要配置 resolver?我在 StackOverflow 搜到了答案,Nginx proxy_pass with $remote_addr

If the proxy_pass statement has no variables in it, then it will use the “gethostbyaddr” system call during start-up or reload and will cache that value permanently.
if there are any variables, such as using either of the following:

set $originaddr http://origin.example.com;
proxy_pass $originaddr;
# or even
proxy_pass http://origin.example.com$request_uri;

Then nginx will use a built-in resolver, and the “resolver” directive must be present. “resolver” is probably a misnomer; think of it as “what DNS server will the built-in resolver use”. Since nginx 1.1.9 the built-in resolver will honour DNS TTL values. Before then it used a fixed value of 5 minutes.

proxy_pass 值没有变量时,会在 Nginx 启动时用 gethostbyaddr 这个系统调用来做一次解析,之后一直保存着这个解析结果。而当 proxy_pass 值中包含变量,即便变量并不处于域名中,就要用 resolver 指令指定的 resolver 来解析对应的域名。

修改 shortcode

最后修改 shortcode,把获取到的豆瓣图片地址替换掉。把两处分别对应电影和图书的代码做了如下修改。

// ...
// ...
success: function (data) {
    ...
    var db_image = data.images.large.replace('.doubanio.com', '-douban.ifttl.com')
    $('#db' + id).html(
        // ...
        // ...
        "");
}

最后的最后,我又给我的反向代理地址加了层 Cloudflare,又多了一层缓存,并且捎带着给这个地址用上 HTTPS。也是为了 Cloudflare 的 HTTPS,我用的域名是 *-douban.ifttl.com,而不是 *.douban.ifttl.com,因为 Cloudflare 只免费给一级子域配置 HTTPS 证书。

一番折腾下来,我要承认,把

改成

然后再修改 CSS,这样更省事,并且也很有效。但是我那么搞一通,给豆瓣服务器减小压力啊。