Rails Nginx + Rails 下如何进行文件的安全下载?

zamia · 2016年02月04日 · 最后由 ghn645568344 回复于 2016年10月21日 · 7767 次阅读
本帖已被管理员设置为精华贴

问题

一般常见的文件下载有两类需求:

  • 公开的文件下载,比如 rails 的 assets、或者用户上传的一些可以公开的文件,比如自己的头像;
  • 较隐私的文件下载,使用场景也不少,比如某些企业后台上传的用户认证资料;

前一类需求 rails 中比较容易实现,一般直接把用户上传的文件存储目录直接放在 /public/some/dir 中,然后按照日期或者 ID 之类的做个目录结构也就够用了。

对于后一类场景,具体的需求有两点:

  1. 必须经过用户认证和授权,检查认证和权限之后才能访问文件;这个就是典型的业务层需要处理的,也就是 app server(rails 层) 需要实现的;
  2. 保证速度,不能给 app server 过多负担。这个是 web server 擅长做的,比如 nginx 可以使用系统的 sendfile 功能可以直接从文件系统发送至网络层,可以少 2 次的内存 copy;web server 也可以做一些缓存等等;

根据上面的需求,文件的安全下载实现起来稍微麻烦一点,不过根据『这么通用的需求一定有现成的解决方案』的原则,其实配置和使用起来还是比较简单的,这篇小文就总结下使用 nginx 和 rails 配合、利用 X-Accel(一般也称作 X-Sendfile)来实现隐私文件的安全下载。

底层原理和实现

这是典型的 web server 和 application server 互相配合的过程,内部的访问过程 这篇文章 也讲的比较清楚,下面的总结更详细一些,补充了一些源码和日志,能帮助大家彻底搞清楚整个过程。

  1. 浏览器访问一个地址,比如 /download/files/123,这个地址对应一个需要验证权限的文件;

    GET /download/files/123 HTTP/1.1
    
  2. nginx 收到这个请求之后根据路由配置,把请求转发到 rails。

    nginx 转发的时候根据配置,添加上两个参数,转发给 rails。后端 rails 服务器会根据这两个参数来对 response body 进行修改,后面会提到。

    GET /download/files/123 HTTP/1.0
    X-Forwarded-For: 127.0.0.1
    X-Sendfile-Type: X-Accel-Redirect
    X-Accel-Mapping: /var/www/fishtrip/private=/private
    

    上面的参数 X-Sendfile-Type 告知后端 nginx 支持什么样的参数(像 apache、lighttpd 支持的参数名称不同);参数 X-Accel-Mapping 告知后端应该怎么样做文件名称的 mapping。

  3. rails 收到这个请求之后,正常流进某个 controller#action,经过业务代码的判断之后,找到这个 url 对应的真正的文件名,然后使用 sendfile 发送文件。

    def show
        pic = File.find params[:id]
        send_file pic.path, type: "image/jpeg", disposition: 'inline'
    end
    

    其实在整个过程中,rails 的背后是 Rack::Sendfile 这个 middleware 在工作。看看它的源码中的 call 函数的实现:

    # File rack/lib/rack/sendfile.rb
    case type = variation(env)
    when 'X-Accel-Redirect'
     path = F.expand_path(body.to_path)
     if url = map_accel_path(env, path)
       headers['Content-Length'] = '0'
       headers[type] = url
       body = []
     else
       env['rack.errors'].puts "X-Accel-Mapping header missing"
     end
    when ...
    end
    # some code here
    

    可以看到这个 middleware 吧 content-length 置为 0,把 body 置空,返回给前端一个计算过 mapping 的 url。

    其中的函数 map_accel_path 是 private 函数,长这样:

    def map_accel_path(env, file)
      if mapping = env['HTTP_X_ACCEL_MAPPING']
        internal, external = mapping.split('=', 2).map{ |p| p.strip }
        file.sub(/^#{internal}/i, external)
      end
    end
    

    其实就是根据 nginx 传入的 X-Accel-Mapping 参数把实际的地址替换成一个 mapping 地址。

    所以,这样也就不难猜测我们自己写的 action 里面的 sendfile 的实现了,sendfile 只需要实现一个支持 to_path 调用的对象即可。去 Rails 中看看它的实现:

    # File actionpack/lib/action_controller/metal/data_streaming.rb
    def send_file(path, options = {}) #:doc:
     # 省略一些代码
     self.status = options[:status] || 200
     self.content_type = options[:content_type] if options.key?(:content_type)
     self.response_body = FileBody.new(path)
    end
    

    里面的 FileBody 类就支持 to_path 调用;

    所以,经过 Rails 和 Rack::Sendfile 的配合,rails 返回给 nginx 的就是一个没有 body,只有 headers 的 response,长下面这个样子(来源于 nginx 的 debug 日志,略去了部分内容):

    http proxy header: "Content-Disposition: inline; filename="abc.jpg""
    http proxy header: "Content-Transfer-Encoding: binary"
    http proxy header: "Content-Type: image/jpeg"
    http proxy header: "Content-Length: 0"
    http proxy header: "X-Accel-Redirect: /private/files/abc.jpg"
    
  4. nginx 收到 rails 返回的数据之后,会检查 X-Accel-Redirect 参数的值,然后内部再根据 location 的配置进行内部跳转,找到真正的文件地址(需要配置,见下文),并且调用操作系统的 sendfile 接口,直接返回给用户。

这个就是整个文件安全下载的过程了,这个过程涉及到 nginx - rails - Rack::Sendfile - nginx 的这么一个过程,看起来有点复杂。实际上我们使用的时候配置起来还是比较简单的。

实现和配置

配置主要是两部分,nginx 和 rails 的部分,如果使用 capistrano 部署线上服务,因为涉及到软链的问题,所以配置略有不同。

nginx

根据上面的原理部分的描述,nginx 的配置也分为两个部分:

  1. 跳转后端时参数配置

    set $app_root /var/some/dir;
    
    location /download {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Sendfile-Type X-Accel-Redirect;
            proxy_set_header X-Accel-Mapping "$app_root/private=/private";
    
            proxy_set_header Host $http_host;
            proxy_redirect off;
            expires off;
    
            proxy_pass http://backend;
    }
    

    这个配置的作用是 nginx 在把请求转发给 rails 后端的时候添加 X-Senfile-Type 和 X-Accel-Mapping 参数;

  2. 收到后端回复后内部地址的配置

    location /private {
            internal;
            alias $app_root/private;
    }
    

    这个配置的作用是 nginx 收到 rails 后端返回的值时可以正确找到文件的实际地址;

rails

rails 的配置就更简单了,添加一句话即可:

config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'

capistrano

capistrano 的部署使用了软链的方法,所以上面 nginx 配置的地方,需要添加一个正则即可。这样后端 rails 就可以正常的去 mapping 了(也就是 Rack::Sendfile 里面的 map_accel_path 是支持正则的):

proxy_set_header X-Accel-Mapping "$app_root/releases/\d{14}/private=/private";

如果实际部署情况跟这个不一致,只要走类似的方法就行了,你懂的~

总结

虽然文件的安全下载是一个小功能,而且现在文件的云存储很多(大鱼也迁移到了云存储上...),但是通过这里例子也可以看看 web server 和 application server 是如何配合工作的,反向代理的很多功能也都是类似机制完成的。

通过这个例子也可以了解一下 rack 的工作机制,可以看到如果通过简单的代码来实现一个相对复杂的功能。

以上~

参考文章

  1. http://thedataasylum.com/articles/how-rails-nginx-x-accel-redirect-work-together.html
  2. https://gist.github.com/Djo/11374407
  3. https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
  4. http://airbladesoftware.com/notes/rails-nginx-x-accel-mapping/
  5. http://www.rubydoc.info/github/rack/rack/Rack/Sendfile

这个事情说白了就是前端请求过来,后端 rails 验证一下,通过了就利用 nginx 的内部跳转给跳到正确的文件路径,通不过就直接返回 403 之类的响应吧。

文章开头建议说明一下 X-Accel 是 nginx 的 module,顺便带一句 X-Accel 的基本原理,理解起来可能会更容易一点(也可能我水平比较差 QAQ)。

#3 楼 @assyer 总结的很对~ 这样确实更容易理解,不过主要是想说明一下 nginx 和 rails 内部的参数传递过程、以及 rack::sendfile 起到了什么作用

5 楼 已删除

还有一个好处就是可以实现 ZeroCopy。

类似阿里云存储或者七牛这种云存储有什么办法呢?既想文中的效率,又不想直接使用公开的 bucket?

总结得比较实用

#7 楼 @xueron 你看文档啊.... 后面加 ?attname= 就可以了

弱弱的问一声 为什么我这没有 File.find 这个方法呢?

ghn645568344 文件的安全下载,报 Cannot read file 提及了此话题。 10月21日 15:05
需要 登录 后方可回复, 如果你还没有账号请 注册新账号