背景
防盗链,其本质就是用户对于自己的资源设置的访问控制,控制“谁”可以在“什么时间”访问到“什么资源”。不做防盗链,用户的许多资源都为其他人做了嫁衣,也会给自己的服务器增加不必要的访问压力和带宽消耗。
不同的用户,由于网站的性质不同(游戏/新闻/游戏),需求也是不尽相同的,所以需要在我们的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,来达到域名黑白名单的效果,样例:
# 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; } }
# conf/static_server_names.conf; server_name a.com b.com ;
nginx module
ngx_http_referer_module,可以专门用于判断referer是否合法、或者是否是空的referer,样例:
location ~* .(gif|jpg|png|bmp)$ { valid_referers none blocked *.mmtrix.com server_names ~.google. ~.baidu.; if ($invalid_referer) { return 403; } }
ngx_http_secure_link_module,用于检测访问资源者的授权,以及资源的有效时间,可以用于Token加密串的防盗链,样例:
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; } } }
nginx + lua
虽然nginx的功能已经比较强大,但是不够灵活,而且在conf中有过多的配置,也会占用较多内存、影响nginx处理请求的时间。我们既然作为CDN服务提供商,除了全局的配置,还需要为每个用户的每个域名进行自定义的配置,因为将所有条件写入nginx conf是不现实的。另外,lua还提供更多强大功能,例如请求时访问redis、memcached,和其他实例共享状态等等功能。因此,nginx + lua可能是一个不错的解决方案。
网上的cookie token加密串的样例:
-- 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/")
网上查找到的各种与nginx+lua相关的方案,基本上都还是单机版本的解决方案。考虑到我们分布式环境,以及业务复杂的情况,还需要用到分布式存储、缓存、任务分发等技术。