背景

防盗链,其本质就是用户对于自己的资源设置的访问控制,控制“谁”可以在“什么时间”访问到“什么资源”。不做防盗链,用户的许多资源都为其他人做了嫁衣,也会给自己的服务器增加不必要的访问压力和带宽消耗。

不同的用户,由于网站的性质不同(游戏/新闻/游戏),需求也是不尽相同的,所以需要在我们的portal系统中添加访问控制的功能,满足用户的需要。

业务功能

防盗链生效配置

表示在什么情况下需要进行防盗链逻辑。比如我们需要对 xxx.com/image/ 下面的URL进行防盗链处理,而对 xxx.com/js/

是否进行防盗链,一般通过2种方式进行匹配:

  • URL匹配
  • 类型匹配

URL匹配是指,符合某些前缀或者正则表达式的URL进行防盗链,见上例; 类型匹配,就是对某些特定类型的资源进行防盗链,例如jpg、mp3。

防盗链规则配置

IP黑白名单

只允许或者阻止某些IP访问资源,一般都是IP黑名单的形式。

用户可以在界面中输入单个IP、IP区间、或者IP通配符,例如:

1.2.3.4
192.168.199.1 ~ 192.168.199.100
192.168.1.*

个人理解是我们通过分析以往的访问日志,得出某些IP的访问可能有异常,之后将其加入黑名单。IP黑名单的限制范围既可以是全局范围(我们CDN服务提供商配置),也可以是域名范围(CDN服务使用方配置)。


Referer黑白名单

最常用的防盗链手段,通过对HTTP请求中的referer header进行判断,以决定用户是否可以访问该资源。一般都是referer白名单的形式。

需要支持单个域名以及泛域名的形式,例如:

ent.cankaoxiaoxi.com
*.cankaoxiaoxi.com
public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 获取请求是从哪里来的
        String referer = request.getHeader("referer");
        // 如果是直接输入的地址,或者不是从本网站访问的重定向到本网站的首页
        if (referer == null || !referer.startsWith("http://localhost")) {
            response.sendRedirect("/day06/index.jsp");
            // 然后return,不要输出后面的内容了
            return;
        }
        String date = "日记";
        response.getWriter().write(date);
    }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

 

禁止空Referer

禁止空referer,即不允许用户直接访问该资源,因为直接在浏览器的地址栏中输入一个资源的URL地址,请求是不会包含Referer字段的。

个人认为使用场景并不多。

UA白名单

HTTP请求header中的User Agent字段,是一段浏览器或者设备标识自己的字符串。对于网站主来说,有时需要让一些资源只能在某些浏览器或者设备上才能访问。

UA 防盗链通常用在手机 APP 或者一些可自定义 User Agent 的应用,比如播放器。设置UA白名单,也和其他情况类似,支持字符串通配符。

Java通过浏览器请求头(User-Agent)获取 浏览器类型,操作系统类型,手机机型

User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。
一些网站常常通过判断 UA 来给不同的操作系统、不同的浏览器发送不同的页面,因此可能造成某些页面无法在某个浏览器中正常显示,但通过伪装 UA 可以绕过检测。

一:获得浏览器请求头中的User-Agent
String ua = request.getHeader("User-Agent")

二:获得浏览器类型,操作系统类型:(注意,UserAgent类在UserAgentUtils.jar中,自行下载)
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));  
       Browser browser = userAgent.getBrowser();  
       OperatingSystem os = userAgent.getOperatingSystem();

三:获得手机类型:

方案一:正则表达式

通过观察规律,得出以下表达式:
;\s?([^;]+?)\s?(Build)?/

Java代码:
Pattern pattern = Pattern.compile(";\\s?(\\S*?\\s?\\S*?)\\s?(Build)?/");  
            Matcher matcher = pattern.matcher(userAgent);  
            String model = null;  
            if (matcher.find()) {  
                model = matcher.group(1).trim();  
                log.debug("通过userAgent解析出机型:" + model);  
            }

以下为部分UserAgent,供测试,可以直接在EditPlus里验证。
通过验证,成功率95%以上。

 

方案二:开源类库WURFL

地址:https://wurfl.sourceforge.net/apis.php

在线测试地址:https://tools.scientiamobile.com/

 

Token加密串

Token加密串,是通过资源URL、密钥、以及过期时间生成的一个加密字符串,然后外链必须要带上这个 Token 才能在规定的时间内访问到该资源。

token一般会在请求参数或者cookie中,没有带 token 的外链,或者超过了有效期的token链接都会返回403,达到防盗链的目的。

Token 加密串这种方案,一般用于文件下载的场景比较多,静态图片的场景则较少。

盗链提示

当盗链发生时,一般会在页面显示出一个固定的文本或者图片,用于提示用户该资源为盗链。

在系统中,用户需要有界面能够上传自定义的盗链提示图,甚至是不同的HTTP返回码可以设置不同的盗链提示图。

 

相关技术

nginx conf

nginx本身已经提供了丰富的功能,通过获取HTTP请求中的各个字段,判断请求是否合法,可以实现以上提到的URL匹配、类型匹配、IP黑白名单等功能。

比如,可以获取client ip,来决定是否请求upstream,以此达到IP黑白名单的目的,样例:

## If IP is 1.2.3.4 send backend to apachereadwrite ##
if ( $remote_addr ~* 1.2.3.4 ) {
    proxy_pass http://cdn_backend;
}

也可以判断server_name,来达到域名黑白名单的效果,样例:

防盗链java 防盗链接_白名单

# conf/nginx.conf

server {
    listen       80;
    include      /home/uaq/local/static_dynamic_proxy/nginx/conf/static_server_names.conf;

    #charset koi8-r;

    #access_log  logs/host.access.log  main;

    location ~ {
        proxy_pass              http://static_accelerate;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

防盗链java 防盗链接_白名单

防盗链java 防盗链接_白名单

# conf/static_server_names.conf;

server_name
    a.com
    b.com
    ;

防盗链java 防盗链接_白名单

nginx module

ngx_http_referer_module,可以专门用于判断referer是否合法、或者是否是空的referer,样例:

防盗链java 防盗链接_白名单

location ~* \.(gif|jpg|png|bmp)$ {
    valid_referers none blocked *.mmtrix.com server_names ~\.google\. ~\.baidu\.;
    if ($invalid_referer) {
        return 403;
    }
}

防盗链java 防盗链接_白名单

ngx_http_secure_link_module,用于检测访问资源者的授权,以及资源的有效时间,可以用于Token加密串的防盗链,样例:

防盗链java 防盗链接_白名单

server {

    listen       80;
    server_name  cdn.aaa.com;
    access_log  /data/logs/nginx/access.log  main;

    index index.html index.php index.html;

    location / {
        secure_link $arg_st,$arg_e;
        secure_link_md5 some_private_key$uri$arg_e;

        if ($secure_link = "") {
            return 403;
        }

        if ($secure_link = "0") {
            return 403;
        }
    }
}

防盗链java 防盗链接_白名单

nginx + lua

虽然nginx的功能已经比较强大,但是不够灵活,而且在conf中有过多的配置,也会占用较多内存、影响nginx处理请求的时间。我们既然作为CDN服务提供商,除了全局的配置,还需要为每个用户的每个域名进行自定义的配置,因为将所有条件写入nginx conf是不现实的。另外,lua还提供更多强大功能,例如请求时访问redis、memcached,和其他实例共享状态等等功能。因此,nginx + lua可能是一个不错的解决方案。

网上的cookie token加密串的样例:

防盗链java 防盗链接_白名单

-- Some variable declarations.
local cookie = ngx.var.cookie_MyToken
local hmac = ""
local timestamp = ""

-- Check that the cookie exists.
if cookie ~= nil and cookie:find(":") ~= nil then
    -- If there's a cookie, split off the HMAC signature
    -- and timestamp.
    local divider = cookie:find(":")
    hmac = cookie:sub(divider+1)
    timestamp = cookie:sub(0, divider-1)

    -- Verify that the signature is valid.
    if hmac_sha1("some very secret string", timestamp) == hmac and tonumber(timestamp) >= os.time() then
        return
    end
end

-- Internally rewrite the URL so that we serve
-- /auth/ if there's no valid token.
ngx.exec("/auth/")

防盗链java 防盗链接_白名单

网上查找到的各种与nginx+lua相关的方案,基本上都还是单机版本的解决方案。考虑到我们分布式环境,以及业务复杂的情况,还需要用到分布式存储、缓存、任务分发等技术。