继mysql迁移完成之后,原来在阿里云对象存储的文件迁移也应该提上日程。

在进行文件迁移之前,首先应该解决好对象存储的代理问题,首先之所以要用腾讯云轻量代理腾讯云对象存储主要从两方面来考虑,首先是减少费用,根据腾讯云cos的计费说明,产生的费用主要是由三部分组成,存储费、流量费和请求次数费。其中存储费用是按照存储量与使用时间来收费的,流量费主要是按照直接从腾讯云对象存储服务器流向公网的流量来收费的,一般在0.5元/GB以上,比较贵,但是如果是从腾讯云cos经由腾讯云内网流向腾讯云的服务器,这部分流量是不收费的,所以通过腾讯云轻量服务器的代理是可以节约费用的。其次是防范风险,如果cos存储桶开放了公共读的权限,一些大文件有可能会被别有用心的人反复下载,造成巨额的流量费用,将存储桶设置为私有读并通过腾讯云轻量对其代理签名可以有效防范被盗刷的风险。

之前用阿里云OSS时,通过网上的一个lua脚本使用nginx即可完成对阿里云OSS的请求签名与代理,现在迁移到腾讯云COS,也可以采用同样的方法来进行代理与签名认证。按照目前互联网通行的S3兼容对象存储标准,设置为私有读写的存储桶需要用密钥进行签名才能进行正常的读写,签名的那一串字符串一般来说既可以放在请求的header里面,也可以以url参数的形式传递给COS服务器。一般来说,为了防止url里的参数解析出错,通常把签名内容放进header里面。

具体的签名过程比较复杂,至少比阿里云的oss复杂多了,具体可以看腾讯云的签名文档,里面有八大步骤,同时还给出了一些常见语言的SDK或者是示例,显然里面没有关于lua的。之所以用lua是因为lua作为一种高效的脚本语言可以集成到同样高效的nginx里面,通过包管理(Ubuntu/Debian)直接安装 libnginx-mod-http-lua即可,无需额外的操作。同时,如果将lua与nginx进行深度整合,就成了OpenResty了,这个从某种意义上来说已经是另一套东西了。

过于腾讯云对象存储COS的代理签名,大约在一年前有大佬写过一个OpenResty模块,如果有OpenResty可以直接用,但考虑到已经安装好了nginx,再改成OpenResty太麻烦而且还不一定成功,直接当成纯lua弄到nginx里面肯定会缺依赖,于是决定将原来的阿里云的那个纯lua脚本改一改,改成腾讯云COS版本的。

首先要解决依赖问题,原来的那个阿里云的lua脚本没有引入任何一个外部依赖,完全由nginx与基本的lua就够了,当然这是因为阿里云OSS的签名机制比较简单,看了一下lua-resty-tencent-cos-signature模块,里面引入了两个依赖,分别是lua-resty-string和lua-resty-hmac,后一个是用来计算sha1签名的,然而原来的阿里云OSS签名也需要计算这个签名,实际上直接用nginx提供的ngx.hmac_sha1这个函数来计算签名值。在nginx里的lua里面的ngx实际上是nginx的借口,通过ngx可以获得nginx的一些信息与调用一些nginx的功能。前一个依赖看起来像是字符串操作相关的,实际上用到的功能是将计算完成的签名值由二进制数据转换成16进制的字符串。这个转化在阿里云的那个脚本里是没有的,以为阿里云OSS的签名使用base64编码的,这个功能在ngx里面直接用。到这里似乎陷入了僵局,lua-resty-string只能在OpenResty框架下解决,然而在包管理里面还是有一个lua-nginx-string只要一键安装了这个,就可以用里面的二进制转16进制字符串的功能了。

所以依赖的问题就这样很好解决了,只需要

sudo apt install libnginx-mod-http-lua 
sudo apt install lua-nginx-string

解决完依赖之后只需要稍微改一下计算签名的脚本即可,主要改一下依赖,只保留local str = require('nginx.string')这一个依赖即可。同时还改了计算签名所使用的函数和API密钥的传递方式,可以直接写入nginx配置文件无需经过环境变量。

nginx配置

server {
    listen 443 ssl http2;
     server_name a.neko.red;
    ssl_certificate     /root/key/dog.pem;
    ssl_certificate_key /root/key/dog.key;
  
        add_header 'Access-Control-Allow-Origin' '*'; #跨域用
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; #跨域用    
        add_header 'Access-Control-Allow-Headers' 'Content-Type'; #跨域用
        proxy_intercept_errors on;
        location / {
            set $cos_access_key "";#access_key
            set $cos_secret_key "";#secret_key
            rewrite_by_lua_file "/etc/nginx/lua/cos.lua";#脚本放置位置
    }
        # internal redirect
        location @cos {
            proxy_pass https://<bucketname-appid>.cos.ap-beijing.myqcloud.com; #backet访问地址
            proxy_set_header   Host   <bucketname-appid>.cos.ap-beijing.myqcloud.com; #也可以换成自定义域名
        }
}

lua脚本

local str = require('nginx.string')

local function urlEncode(s)
  s = string.gsub(s, "([^%w%.%- ])", function(c) return string.format("%%%02X", string.byte(c)) end)
 return string.gsub(s, " ", "+")
end

local function urlDecode(s)
 s = string.gsub(s, '%%(%x%x)', function(h) return string.char(tonumber(h, 16)) end)
 return s
end

function get_sha1(string)
    local sha1 = ngx.sha1_bin(string)
    return str.to_hex(sha1)
end
-- 步骤1:生成 KeyTime

function get_key_time(available_period)
  --local StartTimestamp = os.time()
  local StartTimestamp = ngx.time()
  local EndTimestamp = StartTimestamp + available_period
  local KeyTime = tostring(StartTimestamp) .. ';' .. EndTimestamp
  return KeyTime
end

-- 步骤2:生成 SignKey

function get_sign_key(key,key_time)
  local returndog=ngx.hmac_sha1(key,key_time)
  return str.to_hex(returndog)
end


-- 步骤3:生成 UrlParamList 和 HttpParameters
-- UrlParamList:delimiter;max-keys;prefix
-- HttpParameters:delimiter=%2F&max-keys=10&prefix=example-folder%2F
local function get_param_list()
    --local arg = ngx.req.get_uri_args()
    local arg = {}
    -- local arg = {
    --  ['prefix'] = 'example-folder%2F',
    --  ['max-keys'] = '%2F'
    -- }
    local UrlParamList = ''
    local HttpParameters =''
  
    local arg_key_list = {}
  
    for k in pairs(arg) do table.insert(arg_key_list, k) end
    table.sort(arg_key_list, function(a, b) return a:upper() < b:upper() end)
  
    UrlParamList = table.concat(arg_key_list, ";")
  
    for kid,kvalue in pairs(arg_key_list) do
      local and_c = ''
      if (kid ~= 1)
      then
        and_c = '&'
      end
      HttpParameters = HttpParameters .. and_c .. kvalue .. "=" .. urlEncode(urlDecode(arg[kvalue]))
    end
  
    return {
      UrlParamList = UrlParamList,
      HttpParameters = HttpParameters
    }
    -- return UrlParamList .. '\n' .. HttpParameters
end
  
  -- 步骤4:生成 HeaderList 和 HttpHeaders
  -- HeaderList = date;host;x-cos-acl;x-cos-grant-read
  -- HttpHeaders = date=Thu%2C%2016%20May%202019%2003%3A15%3A06%20GMT&host=examplebucket-1250000000.cos.ap-shanghai.myqcloud.com&x-cos-acl=private&x-cos-grant-read=uin%3D%22100000000011%22
  local function get_header_list()
    --local arg = ngx.req.get_headers()
    local arg = {}
    -- local arg = {
    --  ['prefix'] = 'example-folder%2F',
    --  ['max-keys'] = '%2F'
    -- }
    local HeaderList = ''
    local HttpHeaders =''
  
    local arg_key_list = {}
  
    for k in pairs(arg) do table.insert(arg_key_list, k) end
    table.sort(arg_key_list, function(a, b) return a:upper() < b:upper() end)
  
    HeaderList = table.concat(arg_key_list, ";")
  
    for kid,kvalue in pairs(arg_key_list) do
      local and_c = ''
      if (kid ~= 1)
      then
        and_c = '&'
      end
      HttpHeaders = HttpHeaders .. and_c .. kvalue .. "=" .. urlEncode(urlDecode(arg[kvalue]))
    end
  
    return {
      HeaderList = HeaderList,
      HttpHeaders = HttpHeaders
    }
end

-- 步骤5:生成 HttpString
-- HttpMethod\nUriPathname\nHttpParameters\nHttpHeaders\n
local function get_http_string(HttpMethod, HttpParameters, HttpHeaders)
    local UriPathname = ngx.var.uri
    -- local UriPathname = '/'
    local line_break = '\n'
    return HttpMethod:lower() .. line_break .. UriPathname .. line_break .. HttpParameters .. line_break .. HttpHeaders .. line_break
    -- HttpMethod\nUriPathname\nHttpParameters\nHttpHeaders\n
  end

  -- 步骤6:生成 StringToSign
-- sha1\nKeyTime\nSHA1(HttpString)\n
local function get_string_to_sign(KeyTime, HttpString)
    local line_break = '\n'
    local SHA1 = get_sha1(HttpString)
    return 'sha1' .. line_break .. KeyTime .. line_break .. SHA1 .. line_break
  end

-- 步骤7:生成 Signature
-- 使用 HMAC-SHA1 以 SignKey 为密钥(字符串形式,非原始二进制),以 StringToSign 为消息,计算消息摘要,即为 Signature,例如:01681b8c9d798a678e43b685a9f1bba0f6c0e012
local function get_signature(SignKey, message)
    return str.to_hex(ngx.hmac_sha1(SignKey, message))
  end

-- 步骤8:生成签名
-- 根据 SecretId、KeyTime、HeaderList、UrlParamList 和 Signature 生成签名
local function get_auth_string(Sha, SecretId, KeyTime, HeaderList, UrlParamList, Signature)
    return 'q-sign-algorithm=' .. Sha
      .. '&q-ak=' .. SecretId
      .. '&q-sign-time=' .. KeyTime
      .. '&q-key-time=' .. KeyTime
      .. '&q-header-list=' .. HeaderList
      .. '&q-url-param-list=' .. UrlParamList
      .. '&q-signature=' .. Signature
  end

function cos_auth()

  local available_period = 60
  local Sha = 'sha1'
  local SecretId = ngx.var.cos_access_key
  local SecretKey = ngx.var.cos_secret_key
  -- 步骤1:生成 KeyTime
  local KeyTime = get_key_time(available_period)
    -- 步骤2:生成 SignKey
  local SignKey = get_sign_key(SecretKey, KeyTime)

  -- 步骤3:生成 UrlParamList 和 HttpParameters
  local UrlParamList = get_param_list()['UrlParamList']
  local HttpParameters = get_param_list()['HttpParameters']
  -- 步骤4:生成 HeaderList 和 HttpHeaders
  local HeaderList = get_header_list()['HeaderList']
  local HttpHeaders = get_header_list()['HttpHeaders']
  -- 步骤5:生成 HttpString
  local HttpString = get_http_string('GET', HttpParameters, HttpHeaders)
  -- 步骤6:生成 StringToSign
  local StringToSign = get_string_to_sign(KeyTime, HttpString)
  -- 步骤7:生成 Signature
  local Signature = get_signature(SignKey, StringToSign)
  -- 步骤8:生成签名
  local AuthString = get_auth_string(
    Sha, SecretId, KeyTime, HeaderList, UrlParamList, Signature
  )
  ngx.req.set_header('Authorization',AuthString)
  ngx.exec("@cos")

end

res = cos_auth()

if res then
   ngx.exit(res)
end