使用OpenResty进行图片代理(破解反盗链)

今天在使用scrapy爬取网站的时候,因为网站的容量有限,不能拉取图片进行存储。但是源站的图片又存在着反盗链的限制,所以利用OpenResty做了一个图片代理的接口,实现了破解反盗链的功能。(如果你要问我为什么使用OpenResty来做,我会告诉你因为网站就是用OpenResty开发的)

lua-resty-http

在OpenResty上面使用代理时,一般情况下会用proxy_pass或者lua-resty-http。当我们使用上面2种方式去实现的时候,会发现其不会解析域名的信息,从而造成无法发起http请求。这里面介绍一下使用动态DNS解析域名信息,然后发起HTTP请求。

首先需要在OpenResty安装目录下面添加lua-resty-http的依赖库:http.luahttp_headers.lua。将这2个文件放在resty下面即可。

下面介绍一下具体的实现,首先提供一个工具类

http_dns.lua

local http = require "resty.http"
local resolver = require "resty.dns.resolver"

local _M = {}

_M._VERSION="0.1"

function _M:http_request_with_dns( url, param )
    -- get domain
    local domain = ngx.re.match(url, [[//([\S]+?)/]])
    domain = (domain and 1 == #domain and domain[1]) or nil
    if not domain then
        ngx.log(ngx.ERR, "get the domain fail from url:", url)
        return {status=ngx.HTTP_BAD_REQUEST}
    end

    -- add param
    if not param.headers then
        param.headers = {}
    end
    param.headers.Host = domain

    -- get domain ip
    local domain_ip, err = self:get_domain_ip_by_dns(domain)
    if not domain_ip then
        ngx.log(ngx.ERR, "get the domain[", domain ,"] ip by dns failed:", err)
        return {status=ngx.HTTP_SERVICE_UNAVAILABLE}
    end

    -- http request
    local httpc = http.new()
    local temp_url = ngx.re.gsub(url, "//"..domain.."/", string.format("//%s/", domain_ip))

    local res, err = httpc:request_uri(temp_url, param)
    if err then
        return {status=ngx.HTTP_SERVICE_UNAVAILABLE}
    end

    -- httpc:request_uri 内部已经调用了keepalive,默认支持长连接
    -- httpc:set_keepalive(1000, 100)
    return res
end


-- 根据域名获取IP地址
function _M:get_domain_ip_by_dns( domain )
  -- 这里写死了google的域名服务ip,要根据实际情况做调整(例如放到指定配置或数据库中)
  local dns = "8.8.8.8"

  local r, err = resolver:new{
      nameservers = {dns, {dns, 53} },
      retrans = 5,  -- 5 retransmissions on receive timeout
      timeout = 2000,  -- 2 sec
  }

  if not r then
      return nil, "failed to instantiate the resolver: " .. err
  end

  local answers, err = r:query(domain)
  if not answers then
      return nil, "failed to query the DNS server: " .. err
  end

  if answers.errcode then
      return nil, "server returned error code: " .. answers.errcode .. ": " .. answers.errstr
  end

  for i, ans in ipairs(answers) do
    if ans.address then
      return ans.address
    end
  end

  return nil, "not founded"
end

return _M

上面提供了请求的入口,提供一个URL和param参数即可。

下面提供了获取图片的HTTP接口,用以提供给自己的网站使用。

proxy.lua

local req = require "dispatch.req"
local httpdns = require "libs.http_dns"

local _M = {}

_M._VERSION="0.1"

-- 获取类型列表
function _M:picture()
	local args = req.getArgs()
	local imgUrl = args['imgUrl']

    local param = {}
    param['method'] = "GET"
    param['headers'] = {
            	['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36',
			  	['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
			  	['Accept-Language'] = 'zh-CN,zh;q=0.9',
			  	['Referer'] = imgUrl
        	}
    local res = httpdns:http_request_with_dns(imgUrl, param)

    ngx.say(res.body)
end

return _M

到这里图片代理接口的工作就结束了,下面还需要对响应头做一些设定。

header_filter_by_lua_file lua/filter/header_filter.lua;

header_filter.lua

-- 设置响应头信息
local uri = ngx.var.request_uri

local result = string.find(uri, "example.com") -- 发现代理链接,则更改响应头信息
if result == nil then -- 普通请求
    ngx.header["Content-Type"] = "text/html;charset=UTF-8"
else -- 代理请求
    ngx.header["Content-Type"] = "image/jpeg"
end

最后在页面端使用如下方式就可以使用代理接口了。

/proxy/picture?imgUrl=http://image.example.com/image.jpg

HTTPS的场景

上面的请求可以代理HTTP的请求,当代理的URL是HTTPS时,需要做一些额外的调整。你可以选择不验证证书的有效性,设置 param.ssl_verify = false 如下所示:


当然也可以设置授信证书,需要增加如下配置:

lua_ssl_verify_depth 3;
lua_ssl_trusted_certificate "/usr/ssl/certs/moguhu.com.pem";

此时需要注意一点的是:如果是针对于网站的证书,导出时必须为 .pem 格式,导出时可以选择FireFox浏览器导出(笔者使用Chrome导出时,格式总是有问题)。导出过程如下所示:






上面的一系列过程中需要注意的是:证书的类型需要选择“X.509含链证书”,文件名也需要重新定义为 .pem 结尾

特殊情况

笔者用上面接口代理图片时发现,URL中是可以自定义一些头信息,但是代码中最终是以解析过的 IP 访问数据的。如果资源文件要求必须以 domain的方式请求,则可以考虑使用如下方式:

# 图片代理
location /proxy/image {
    resolver 8.8.8.8;
    content_by_lua_block {
        local args = ngx.req.get_uri_args()
        local client = require "resty.http":new()
        local param = {}
        param.headers = { ['Referer'] = args['refer'] }
        param.ssl_verify = false
        local res, err = client:request_uri(args['imgUrl'], param)
        if err then
            return {status=ngx.HTTP_SERVICE_UNAVAILABLE}
        end

        ngx.say(res.body)
    }
    header_filter_by_lua_file lua/filter/header_filter.lua;
}

此时前台的使用方式为:

http://127.0.0.1/proxy/image?imgUrl=https://img.moguhu.com/images/666.jpg&refer=https://www.moguhu.com/


参考:《OpenResty最佳实践》

代码段 小部件