请选择 进入手机版 | 继续访问电脑版
 找回密码
 立即注册
搜索

本文来自

安全运维工具

安全运维工具

人已关注

请添加对本版块的简短描述

精选帖子

nginx+lua实现对阿里云OSS文件的实时解密

[复制链接]
2050 abc 发表于 2019-9-18 19:20:41
临时接到一个需求,有一个巨大的资源池,传到阿里云的OSS上了,坑爹的是资源池内的文件全是加密的.临上线了才发现这个坑.为了安全也不能把未加密的文件对外传输.所以就得想办法实时解密.

首先想到的就是通过一个反向代理来转发,代理收到内容后,解密然后再回包.

所以首选Nginx了, 虽然我们一直用Nginx作为网关,但是我对其还处于完全不了解的状态,趁机恶补一下知识, 花了2个小时看了淘宝团队写的Nginx开发从入门到精通,最后发现,添加一个module竟然要重新编译nginx?? What the fuck!这不科学…

于是寻寻觅觅,发现了lua-nginx-module,进而发现了已经打包好的openresty.

之前接触过云风大神写的skynet游戏服务端框架,也是使用lua作为开发语言,使用lua的协程特性可以很方便的把异步代码”逻辑同步化”,不过我这次需要实现的解密功能,是属于典型的CPU密集型的应用,和同步异步没啥关系…
如果用脚本语言实现加解密算法,性能可想而知,不过还好,发现了一个叫lua-resty-nettle的加解密库,加解密算法都是native实现的,lua只是作为胶水.所以性能基本上放心了.
再加上找京东的同事考证了一下,他们内部确实也使用了这个东西,所以稳定性应该是没有问题.

技术方案定了,接下来就是写代码了.

看了一下官方文档,lua-nginx-module提供了很多钩子,可以让你通过lua代码去处理,找出我现在最需要的几个钩子:

body_filter_by_lua
把反向代理获取到的body传入自己编写的lua代码中进行处理,需要注意的是这里传入的是一个一个”chunk”,载入一个页面,可能会分为好几次调用过来,所以我们编写的lua代码需要处理这种情况
rewrite_by_lua_block
可以使用lua代码对url进行重写,需要用这个的原因是因为资源池以前是在windows上,之前大小写不敏感,导致很多地方的url引用不区分大小写也可以正常使用, 所以现在迁移到OSS后,大小写敏感了.为了兼容这种情况,只能把所有的文件名全部转换为小写,然后把所有转发请求的url都rewrite为小写
header_filter_by_lua_block
可以使用lua代码重写http头.因为回包经过解密后length变了,所以需要把content_length设为空,这样浏览器就可以识别body是chunk的方式传输的,还需要把content_encoding设为nil,否则oss会根据大部分浏览器的请求,返回一个gzip压缩过的数据,为了处理方便,这里就强行把gzip给关了当然,通过这个钩子只能处理被代理方回包的http头,具体如何强行关闭gzip,后面再说.
具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
worker_processes  1;
error_log  logs/error.log  info;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
                 proxy_pass     http://资源池的地址;
                 proxy_set_header Accept-Encoding "";
                 rewrite_by_lua_block {
                      ngx.req.set_uri(string.lower(ngx.var.uri))
                 }
                 header_filter_by_lua_block {
                      ngx.header.content_length = nil
                      ngx.header.content_encoding = nil
                 }
                 body_filter_by_lua_file /path/to/lua/resource_decrypt.lua;
        }
    }
}
注意这一行:

1
proxy_set_header Accept-Encoding "";
使用 proxy_set_header 功能,强行屏蔽掉浏览器gzip压缩格式的请求.所以到被代理方,也就是资源池的请求,永远是不压缩的.

因为lua代码比较长,所以为了方便把它放到一个文件里面了, 然后通过body_filter_by_lua_file 指定文件,效果和body_filter_by_lua是一样的

lua代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
local data, eof = ngx.arg[1], ngx.arg[2]
local des = require "resty.nettle.des"
local buffered = ngx.ctx.buffered
local bufferedSize = ngx.ctx.bufferedSize
local packageSize = 2048000 -- 2m
if not bufferedSize then
   bufferedSize = 0
   ngx.ctx.bufferedSize = bufferedSize
end
if ngx.ctx.bufferedSize <= packageSize then
   if not buffered then
      buffered = {}
      ngx.ctx.buffered = buffered
   end
   if data ~= "" then
      buffered[#buffered+1] = data
      bufferedSize = bufferedSize + #data
      if bufferedSize > packageSize then
         ngx.arg[1] = table.concat(buffered)
         ngx.ctx.buffered = nil
         if eof then
            ngx.ctx.bufferedSize = nil
         end
         return
      else
         ngx.arg[1] = nil
      end
   end
else
   ngx.arg[1] = data
end
if eof then
   local whole = table.concat(buffered)
   ngx.ctx.buffered = nil
   ngx.ctx.bufferedSize = nil
   local ds, wk = des.new("password", "cbc", "iv")
   local raw = ds:decrypt(whole)
   ngx.arg[1] = string.sub(raw, 1, #raw - string.byte(raw, #raw))  -- 需要取最后一个字符作为长度,在解密的字符串中删除掉这个长度的字符数
   ngx.arg[2] = true
end
ngx.arg[1]表示当前数据 ngx.arg[2]表示当前数据是否是最后一个chunk.
对ngx.arg[1]赋值表示要传给最终用户(浏览器)的数据,所以处理完数据后别忘了赋值给ngx.arg[1]
因为数据不是一次性发过来的,所以需要进行拼包,如果发现某个文件过大,就不解密直接raw过去
lua代码中用到了ngx.ctx.*, 这个是lua-nginx-module提供的一个global的table,可以用来暂存任意数据.当一个完整的文件没有传送完成时,之前传送的部分就存在这里.

为了解密des,用到了一个第三方lua库,openresty提供了包管理器,可以使用以下命令安装:

1
sudo opm get bungle/lua-resty-nettle
还有一个需要注意的是,des解密后出来的文件总是对不上,后面会莫名其妙多出几个byte,查阅资料后发现,原来PKCS5Padding会填充长度为8的倍数,处理这个问题也很简单,把最后一个byte拿出来作为冗余数据的长度,从解密数据中删除尾部的这个长度的数据即可.

花了一天从确定技术方案到开发完成,保证了项目上线时间.还是蛮有成就感的.
http://lida.site/2017/11/07/%E4%BD%BF%E7%94%A8nginx-lua%E5%AE%9E%E7%8E%B0%E5%AF%B9%E9%98%BF%E9%87%8C%E4%BA%91OSS%E6%96%87%E4%BB%B6%E7%9A%84%E5%AE%9E%E6%97%B6%E8%A7%A3%E5%AF%86/


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表