背景
防盗链,其本质就是用户对于自己的资源设置的访问控制,控制“谁”可以在“什么时间”访问到“什么资源”。不做防盗链,用户的许多资源都为其他人做了嫁衣,也会给自己的服务器增加不必要的访问压力和带宽消耗。
不同的用户,由于网站的性质不同(游戏/新闻/游戏),需求也是不尽相同的,所以需要在我们的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相关的方案,基本上都还是单机版本的解决方案。考虑到我们分布式环境,以及业务复杂的情况,还需要用到分布式存储、缓存、任务分发等技术。