实战 | 文件下载、及浏览器加速导致不能下载的问题
目录
- 1. 版权
- 2. nginx x-accel
- 3. 浏览器加速导致的问题
- 4. DownloadSessionManager
- 5. 关于下载计数
1. 版权
本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/121565484.
文中代码属于 public domain (无版权).
2. nginx x-accel
为减轻处理压力, 一个较好的方法是程序将下载交由nginx 实际执行, 参考:
https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
例如:
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment;filename=xxx")
w.Header().Set("X-Accel-Redirect", "/download/xxx")
w.Header().Set("X-Accel-Limit-Rate", "512000") // 500K/s
在nginx conf 里配置下载目录"download" 的位置. 当下游nginx 收到程序如上设置的header 后, 将接手该文件的下载.
程序在设置header 之前可以根据请求参数进行权限检查, 但不便之处是程序收不到nginx 的处理结果(也就不知道实际下载成功否).
3. 浏览器加速导致的问题
在实践中碰到某些浏览器, 在下载时可以自动启用加速功能, 推测其机制:
- 可能是浏览器内部创建多线程来下载同一文件(Range 分块请求)
- 也可能加速模块在云上(作为下载代理, 可具有缓存功能), 在云上多线程下载后 -> 返给浏览器
加速造成一个问题: 我们系统中下载文件需验证已登录且有权限, 而加速线程发出的请求经测试(一般)不带session cookie, 从而导致下载失败(有时失败若干次后偶尔又会带上cookie).
经简单调查, nginx 默认支持文件的分块(Range) 下载, 以更高效下载且容易续传/出错恢复; 而且据闻一些video 控件在源不支持Range 请求时不能seek, 所以简单地禁止Range 请求可能不是个好办法.
目前大多数程序传递登录信息都是通过cookie session, 虽然这不是http 标准定义的方式. RFC 7235 (HTTP Authentication) 5.1 指向的iana 官方网页注册了Basic/Bearer/Digest/OAuth 等鉴权方式, 没有"session".
经测试, 发现未登录时设置"WWW-Authenticate" header 并返回401 (Unauthorized) 并没有效果.
好在我们系统的下载文件的安全等级不高, 所以想到一个办法: 在浏览器首次请求下载时, 生成一个免登录的下载链接并redirect 回去, 经试验后续请求都会使用这个免登录链接, 这样就可以下载了.
免登录链接 = 原链接 & _dsid=xx.
dsid “download session id”, 在生成免登录链接同时在服务端创建对应的下载会话, 后续带相同dsid 参数的请求使用同一个下载会话. 下载会话的作用:
- 控制有效期(例如5分钟未请求则不再允许使用), 减少安全风险
- 绑定目标下载文件(文件标志存储在下载会话里), 防止例如拿到dsid 后修改原链接的部分指向其他文件
4. DownloadSessionManager
由于下载的并发度不高, 使用 GolangHttpSession-1 数据结构 “概述” 部分描述的数据结构.
import ("container/list""crypto/rand""fmt""sync""time"
)type DownloadSessionManager struct {mu *sync.Mutexm map[string]*list.Element // id => {*dlses}l *list.List // front (active) -> back (inactive).
}type dlses struct {id stringexpire time.Time // 过期时间n int // access count, 0 when createdata string
}const default_dl_duration_min = 5 // 下载会话有效期: 5minfunc NewDownloadSessionManager() *DownloadSessionManager {return &DownloadSessionManager{mu: new(sync.Mutex),m: map[string]*list.Element{},l: new(list.List),}
}
dlses.data 可用于绑定下载文件. dlses.n 可用于下载计数(因为同一个文件可能有很多Range 下载请求, 但总体只能算一次下载).
创建下载会话:
// Create 新建一个下载会话, 并存储data 数据. 返回会话id. 不能生成id 时panic.
func (dsm *DownloadSessionManager) Create(data string) string {dsm.mu.Lock()defer dsm.mu.Unlock()id := ""for i := 0; i < 10; i++ { // (试10次)if s := genDlSesId(); dsm.m[s] == nil {id = sbreak}}if id == "" {panic("cannot create download sesId")}dsm.m[id] = dsm.l.PushFront(&dlses{id: id,expire: time.Now().Add(default_dl_duration_min * time.Minute),n: 0,data: data,})return id
}func genDlSesId() string {// 24位大写, 如"AC58F133FD0E7AAA03A1F5D6"b := make([]byte, 12)if _, err := rand.Read(b); err != nil {panic(err)}return fmt.Sprintf("%X", b)
}
获取下载会话 - 这将在收到免登录下载请求时调用以作验证:
// Get 获取指定下载会话, 返回{access count/int/>=0, data/string}. 如果会话
// 不存在或已过期, 返回nil.
func (dsm *DownloadSessionManager) Get(id string) []interface{} {dsm.mu.Lock()defer dsm.mu.Unlock()dsm.gc() // 同步清理if e := dsm.m[id]; e != nil {ds := e.Value.(*dlses)ds.n++ds.expire = time.Now().Add(default_dl_duration_min * time.Minute)dsm.l.MoveToFront(e) // => 最活跃return []interface{}{ds.n - 1, ds.data}} else {return nil}
}// gc 清理所有过期下载会话 - 调用者需lock.
func (dsm *DownloadSessionManager) gc() {now := time.Now()for e := dsm.l.Back(); e != nil; e = dsm.l.Back() {ds := e.Value.(*dlses)if now.After(ds.expire) { // 过期dsm.l.Remove(e)delete(dsm.m, ds.id)} else {break}}
}
因为下载的并发度不高, 所以简化直接在Get 时做过期处理.
5. 关于下载计数
如前所述, 由于程序转交nginx 下载后无法知道实际的下载结果, 所以无法准确计数. 保守做法可以:
在Get 返回dlses.n == 0 时计数.
本文未分析Range 请求的"Range" 参数.
Stackoverflow 上有一篇使用cookie 侦测实际下载是否开始的文章:
https://stackoverflow.com/questions/1106377/detect-when-browser-receives-file-download
原理是服务端在响应文件内容同时返回一个cookie, 当页面js 能读到这个cookie 时就说明下载已经开始(但是云加速环境是否compatible?). Interesting.
实战 | 文件下载、及浏览器加速导致不能下载的问题相关推荐
- Safari浏览器下载word文件,后缀多拼接了.html,导致打开下载文件乱码
1.遇到的问题 谷歌和ie下载文件正常 Safari浏览器下载word文件,后缀多拼接了.html,导致打开下载文件乱码,下载的文件名称为test.doc.html,手动去掉多余的后缀.html即可正 ...
- 各个浏览器a标签href下载文件链接长度过长,导致下载失败解决方案
一.问题: 开发中遇到下载exel文档:后端小哥返回来base64位的exel文件:刚开始文件比较小:本人使用创建a标签给href纸箱base64位文件 var a = document.create ...
- python电脑下载有问题-Python 解决火狐浏览器不弹出下载框直接下载的问题
用火狐浏览器下载文件,总是遇到这个弹窗问题,如下图: 原因: 使用火狐浏览器,点击下载,弹出下载弹框,使用AutoITLibrary库,能够判断是否弹出了下载弹框,但因为不能定位到下载弹框,导致没有下 ...
- python调用IE浏览器进行数据批量下载小技巧
一.为什么要使用浏览器下载? 使用场景:已经有了大量的数据下载链接信息,这些保存在txt文本中,每一行是一个完整的下载链接地址,很多人首先就会想到,直接使用迅雷批量下载就好了,确实如此,这也是最简单的 ...
- java二进制文件下载到浏览器默认路径
java二进制文件下载到浏览器默认路径 java二进制文件下载到浏览器默认路径.当然可以下载到指定系统指定路径但是作用不大. 下面是通过调用的开放接口拿到的一个FileBinary二进制文件,输出流输 ...
- 微信中点击链接或者扫描二维码直接跳转外部浏览器打开指定网页下载
大家在使用微信推广的时候是不是经常都会遇到推广链接被拦截导致无法下载app的情况,此时用户在微信中打开会提示"已停止访问该网页".这对于使用微信推广的商家来说非常不友好,而且造成的 ...
- Python如何解决火狐浏览器不弹出下载框直接下载
用火狐浏览器下载文件,总是遇到这个弹窗问题,如下图: 原因: 使用火狐浏览器,点击下载,弹出下载弹框,使用AutoITLibrary库,能够判断是否弹出了下载弹框,但因为不能定位到下载弹框,导致没有下 ...
- 图形引擎实战:手游Android端后台下载技术分享
一.功能特点 手游android端后台下载SDK是畅游自主研发的一款移动平台android端后台文件下载工具包,它主要提供网络文件的后台下载功能,功能完善,性能高,可以满足游戏制作有关后台下载文件的需 ...
- Apple Safari 16.5 发布- macOS 专属浏览器 (独立安装包下载)
Apple Safari 16.5 - macOS 专属浏览器 (独立安装包下载) Safari 浏览器 16 for macOS Montery, Big Sur 请访问原文链接:https://s ...
最新文章
- java. xerces转xml_Xerces -C++遇到的xml编码转换问题
- 我的中年危机来得很自然
- linux mount挂载命令(将分区挂接到Linux的一个文件夹下,从而将分区和该目录联系起来)
- android power 按键,Android Framework层Power键关机流程(一,Power长按键操作处理)
- Python内置函数总结
- MINIGUI交叉编译【转】
- LeetCode 394: DecodeString (Java)
- input[type=radio]自定义样式
- 机器学习分类模型评价指标和方法
- 【回归预测】基于matlab麻雀算法优化LSSVM回归预测【含Matlab源码 1128期】
- iptables指南
- 液压传动理论教学实训
- 迈向新征程!2019国际第三代半导体大赛颁奖典礼盛大举办!
- 菱形(两种数组方法)
- 解读品牌KOL运营之路
- uniapp,从文件流获取图片地址,并展示图片
- java split竖线_java对竖线|进行分割(split)操作
- 常见系统安全漏洞及解决方案
- 内存频率,CPU频率,主板频率之间的制约
- 自定义ro.build.fingerprint