ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ASP无组件上传实现原理:从multipart/form-data到AienAspUpload类拆解

ASP无组件上传实现原理:从multipart/form-data到AienAspUpload类拆解 简介艾恩ASP无组件上传类是一套面向ASP开发者的轻量级文件上传解决方案核心是用简单易懂的代码实现常见上传需求避免依赖第三方组件。类支持表单数据提取、自定义保存目录、数据库同步写入、扩展名与大小限制以及原文件名或随机命名等保存方式适合从入门到进阶的ASP学习者与需要快速集成上传功能的站点开发者。资源包共50个文件以asp源码为主另含png、jpg、gif图片资源、js脚本、swf控件、pdf说明文档及vbs更新脚本压缩包仅331KB。其中asp文件包含主上传类和多套示例页面分别演示单文件、多文件、iframe、HTML5及SWFUpload等上传模式便于对照学习。pdf文档说明了类用法及v13.11.25更新中恶意构造上传死循环的修复vbs脚本用于自动更新整体目录按场景划分结构清晰目前已有316人学习浏览适合希望以最小成本实现ASP上传功能并理解其原理的开发者。 我还记得第一次被人问到“你这个上传能不能不用组件”时的场景。那是一个老客户的老服务器IIS 6.0Windows Server 2003网站上要加一个图片上传功能但服务器上既没有注册AspUpload也没有装LyfUpload甚至连外网都不通想下载DLL都成了奢望。那时候我翻出几年没碰的ASP代码开始研究到底能不能不装组件就完成文件上传。后来折腾出一个像样的类也就是艾恩ASP无组件上传类AienAspUpload。今天把这套东西的核心思路、代码逻辑和踩过的坑整理出来给还在维护老ASP项目或者刚接触经典ASP上传的朋友做个参考。先说清楚这个类解决什么问题在服务器环境受限、无法安装 COM 组件的情况下通过纯 ASP 代码解析 HTTP 协议中的二进制流提取客户端上传的文件数据并保存到服务器磁盘。它不依赖第三方组件、不需要额外配置注册只要服务器跑着 ASP 就能用。适合处理中等体量文件单个 10MB 以下比较稳妥、表单字段与文件同时提交的场景。如果你是刚接触 ASP 的老项目维护者或者正在被服务器权限限制逼到墙角这篇文章应该能帮你少走不少弯路。1. 为什么当年必须自己写一个上传类组件授权与服务器环境的死结1.1 第三方上传组件的问题不在技术在环境早年间做 ASP 开发说到文件上传大多数人第一反应是装组件。市面上常见的有 AspUpload、LyfUpload、SA-FileUp 等等这些组件的功能确实齐全能限制大小、能取扩展名、甚至能边传边写进度条。但它们有一个共同的硬伤——必须在服务器上注册 DLL而且往往还是收费的。服务器不是自家的问题就来了。客户的服务器托管在IDC机房你远程桌面过去权限只有 IIS 和网站目录注册组件需要管理员权限或者至少得能写注册表租用虚拟主机的用户更是连系统盘都摸不着。好不容易找人装了组件过了几个月服务器被重装或者迁移机房组件没了上传功能直接挂掉售后问题一个接一个。还有授权问题。商业组件一套License对应一个域名客户网站如果换域名组件就失效又得重新买。这些现实问题让“无组件”这三个字变得格外有吸引力。1.2 无组件上传的本质不是不做文件上传而是不做组件“无组件上传”这个叫法其实有点误导人。它不是说上传过程不依赖任何组件而是说不依赖第三方注册的 COM 组件。因为 ASP 本身如果要读取表单上传的原始二进制数据必须调用 Request 对象的 BinaryRead 方法而要把这些二进制数据流写进文件又绕不开一个东西——ADODB.Stream。ADODB.Stream 是 ADO 自带的一个流对象Windows 系统默认安装不需要额外注册任何 DLL也不涉及授权问题。所以无组件上传的真正路线是用 Request.BinaryRead 拿到完整的 HTTP 请求体包含文件和普通表单字段的混合数据然后用 ADODB.Stream 做中转在内存中把字节流按二进制方式解析、拆分、重组最后把文件部分落盘保存。这么做的好处很直接代码完全是 .asp 文件部署的时候用记事本改一改、传到服务器上就能跑换服务器、换域名、换机房只要有 IIS 和 ASP 支持就没有任何迁移成本。坏处也明显——没做过二进制定位的人面对那一堆看不见的字节很容易被边界字符串、换行符、文件头这些概念绕晕。这正是我这个类要解决的问题。2. 拆开 HTTP 上传的原始数据流boundary 和 multipart/form-data 的真相2.1 你看到的表单背后浏览器到底发出去的是什么先从一个最简单的上传表单说起form actionupload.asp methodpost enctypemultipart/form-data input typetext nameusername / input typefile namemyfile / input typesubmit / /form注意那个enctypemultipart/form-data这是文件上传的关键。普通表单默认的application/x-www-form-urlencoded是简单文本拼接而文件上传必须用 multipart 格式因为文件内容是一堆二进制字节跟普通字符混在一起没法用 URL 编码处理。当用户点了提交浏览器发出的 HTTP 请求体长这样我简化了头部但格式一致-----------------------------7da1faf201fc Content-Disposition: form-data; nameusername 张三 -----------------------------7da1faf201fc Content-Disposition: form-data; namemyfile; filenametest.txt Content-Type: text/plain 这里是文件test.txt的原始内容... -----------------------------7da1faf201fc--每一段内容之间用一个 boundary边界字符串分隔整个请求体以--boundary开头、以--boundary--结尾。浏览器每次生成的 boundary 是一串随机字符所以服务端不能写死必须从请求头里取。2.2 服务端拿到的是什么Byte 数组不是文本ASP 的 Request.BinaryRead 返回的是什么呢是一个二进制字节数组也就是说请求体里所有内容——文本、数字、文件名、文件内容——全部以字节形式混在同一个数组里。你在浏览器里看到“张三”这两个字在数组里可能是0xD5 0xC5 0xC8 0xFDGBK 编码或者0xE5 0xBC 0xA0 0xE4 0xB8 0x89UTF-8 编码。这意味着解析过程中不能把整个字节数组直接转成字符串再切割否则文件里的二进制内容一旦撞上非法字符就会丢字节、截断或乱码。正确做法是始终把数据当作字节流处理只在定位 boundary、name、filename 这些元信息时把局部字节转成文本进行比对。AienAspUpload 的核心逻辑就是围绕字节流做这三件事在整个字节数组里找到两个 boundary 之间即每个字段的起始和结束位置解析字段里的 Content-Disposition 头部判断它是普通表单字段还是文件头如果是文件从头部结束后的位置开始算起到下一个 boundary 之前把那部分字节原封不动地提取出来写成文件。2.3 边界字符串的定位别用 InStr用手写字节比较老手可能觉得这点小问题有什么可讲的但我见过太多新人写无组件上传时在 boundary 定位上栽跟头。ASP 的 InStr 函数是按照文本方式查找的虽然也能用在字节数组转字符串上但性能很差而且容易出错。AienAspUpload 用的是逐字节比较的方式先取出 boundary 的字节数组然后在整个请求体数组里从第 N 个字节开始逐个比对比对上了才把位置记下来。这个方式乍一听很笨尤其是面对 10MB 的文件时逐字节比较听起来好像要循环 1000 万次。但实际上效率没你想的那么低。因为比较的时候是先把局部字节转换成字符串做的首尾匹配加上 ABS内存块比较等优化实测解析 5MB 文件在普通服务器上也就是零点几秒的事可以接受。3. AienAspUpload 的类设计文件和数据一个都不能少3.1 为什么要把表单字段和文件分开处理很多人写上传类只盯着文件怎么落盘忽略了上传请求里还有普通表单字段。但在实际业务中文件很少单独上传总得带点附加信息比如图片对应的新闻标题、附件所属的订单号、文件的分类ID。如果这些字段拿不到文件存得再好也没用。AienAspUpload 的设计从最开始就把这两部分区分开一类存普通字段文本一类存文件对象。类内部用两个集合分别管理对外暴露的属性和方法也是两套。这样业务代码里既可以通过objUpload.Fields(username)拿到表单文本也可以通过objUpload.File(myfile)拿到文件对象。两者互不干扰使用的时候思路清晰。3.2 设计的两个关键抽象虚拟文件对象和表单字段集合对于文件类内部定义了一个虚拟的对象结构VBScript 里没有真正的类我用字典模拟每个文件对象包含六个基本信息FileName客户端提交的原始文件名含路径需要截取FileExt扩展名小写化后的FileSize文件字节数FileTypeContent-TypeContent文件的字节数组保存在内存中SaveToFile方法把字节数组写入磁盘这里有个细节文件内容什么时候读进内存合适我的做法是一次性把文件内容读进来因为无组件上传大多用于服务器端处理中小文件5MB 以下完全可行。如果处理超大文件一次性读入内存会导致 ASP 进程内存暴涨IIS 5.0 隔离模式下可能导致整个进程回收这个问题后面会仔细说。3.3 属性设计尽量贴近使用直觉类的对外属性我尽量做得“像字典一样简单”Dim objUpload Set objUpload New AienAspUpload objUpload.MaxSize 10 * 1024 * 1024 10MB上限 objUpload.AllowExt jpg,jpeg,gif,png,bmp,zip,rar,doc,docx,pdf objUpload.SavePath /uploads/ objUpload.AutoSave True 解析完自动保存用起来就很简单解析完调一次objUpload.Upload(),然后判断objUpload.ErrNum和objUpload.ErrMsg如果没有错误再遍历objUpload.Files集合一个个访问文件对象。有错误则返回错误码错误码的含义类内部全部做了中文明文。4. 核心代码拆解ADODB.Stream 与二进制流的读取方式4.1 读请求体Request.BinaryRead 的容量陷阱读取上传数据的入口很简单Dim lngTotalBytes lngTotalBytes Request.TotalBytes Dim binData binData Request.BinaryRead(lngTotalBytes)但这里面有一个很坑的细节Request.BinaryRead默认情况下最大只能读取约 200KB具体限制取决于 IIS 和 ASP 版本老 IIS 限制更严。如果直接调用传 2MB 的文件就会报错或者只截到一部分。怎么解开这个限制答案是修改服务器上的 AspMaxRequestEntityAllowed 配置项默认值是 204800 字节。IIS 6 里在 IIS 管理器的“属性 - 主头 - 配置 - 选项 - 上传限制”里改IIS 7 要在web.config的system.webServersecurityrequestFilteringrequestLimits节点里改如果用 IIS 的 metabase可以直接用脚本设置。只改 Request.BinaryRead 还不够因为整个请求体的读取还被 ASP 的 post 大小限制约束。AienAspUpload 的做法是在文档和注释里强烈提示类本身只负责解析不负责解除 IIS 限制部署时一定要配好服务器端的请求限制否则小文件能传、大文件静默失败。4.2 用字节数组做拆分核心处理流程解析函数是整个上传类的核心流程大概分这几步第一步从请求头的 Content-Type 里提取 boundary。注意是从请求头取不是从请求体取因为请求体里第一行的 boundary 已经是在数据区了。Dim strContentType strContentType Request.ServerVariables(HTTP_CONTENT_TYPE) 形如: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW Dim strBoundary strBoundary Mid(strContentType, InStr(strContentType, boundary) 9)第二步把--boundary转换为字节数组。因为请求体是--boundary加两个短线起的解析的时候要找的是--加 boundary以及末尾的--。两个字符连在一起时--boundary--表示这是最后一段。第三步循环在字节数组里查找每个字段。从当前位置往后找--boundary的下一个出现位置中间这一段就是一个完整的 field 块交给后续函数解析这个块的内容。第四步解析单个 field 块的元信息。这一小段的内容看起来像文本但其实也混有二进制文件名可能是 UTF-8 编码。做法是把这个块转成文本但只转前几百个字节因为元信息肯定在开头。转出来之后用正则或者 InStr 提取name...和filename...。如果正则提取不到 filename这就是普通字段提取到了这就是文件字段。第五步普通字段直接取 value 即可文件字段则要跳过头部、从头部结束后的那一串字节截取到 block 末尾把这一大段字节赋给文件对象的 Content 属性。4.3 写文件用 ADODB.StreamType1 意味着什么解析出文件内容字节数组以后还不能直接用 FSO 写文件。因为 FSO 的 CreateTextFile 是按文本模式写的文件字节流里可能有0x00、0xFF这些非可见字符经过 FSO 处理就会出错。正确做法是用 ADODB.Stream打开后先把 Type 设为 1adTypeBinary再写入Dim objStream Set objStream Server.CreateObject(ADODB.Stream) objStream.Type 1 二进制模式 objStream.Open objStream.Write objFile.Content objStream.SaveToFile strFullPath, 2 2表示覆盖 objStream.Close Set objStream Nothing这中间有个容易出错的地方如果文件字节数组本身末尾已经包含了该文件块的内容写文件时必须保证内容部分和 boundary 部分不粘连。我在解析的时候采用末尾回溯的方式把最后可能的\r\n--boundary这些字节从文件内容里剔除确保保存后文件的大小和原始文件一模一样不多一个字节也不少一个字节。5. 从表单到落盘一次完整的调用示例5.1 前端表单的写法注意点上传页面的 HTML 没什么新鲜的但有几个细节会影响服务端解析结果form actionupload.asp methodpost enctypemultipart/form-data nameform1 用户名: input typetext nameusername /br / 选择文件: input typefile namemyfile /br / input typesubmit value上传 / /form这里必须强调method必须是postenctype必须是multipart/form-data这两个属性缺一不可。有人把 enctype 写成multipart/form-data的时候好奇心重在后面加空格或者大小写不一致浏览器发送的 Content-Type 里 boundary 就会出问题服务端解析就会失败。实际测试中遇到过用户把属性写成multipart/form-data; charsetutf-8的这种写法在服务端提取 boundary 时会多出东西需要类内部做容错处理。AienAspUpload 在提取 boundary 时会把分号也作为边界处理比较稳妥。5.2 接收端 upload.asp 的核心调用代码接收端代码用起来很简单!-- #include fileAienAspUpload.asp -- % Dim objUpload Set objUpload New AienAspUpload 配置 objUpload.MaxSize 5 * 1024 * 1024 单文件最大5MB objUpload.AllowExt jpg,gif,png,bmp objUpload.SavePath Server.MapPath(/uploads) 执行解析 objUpload.Upload() 判断结果 If objUpload.ErrNum 0 Then Dim i For i 0 To objUpload.Files.Count - 1 Set objFile objUpload.Files(i) objFile 已经自动保存到 objUpload.SavePath 下了 Response.Write(原文件名: objFile.FileName br/) Response.Write(扩展名: objFile.FileExt br/) Response.Write(大小: objFile.FileSize 字节br/) Response.Write(新文件名: objFile.SaveName br/) Next Response.Write(上传成功) Else Response.Write(错误 objUpload.ErrNum objUpload.ErrMsg) End If Set objUpload Nothing %这里有个细节我特意处理过文件保存到服务器以后用的不是客户端原始文件名而是类自动生成的一个随机文件名避免重名覆盖或者中文文件名带来的存储问题。生成的规则是年 月 日 时 分 秒 随机数 扩展名这样基本上不会撞名。5.3 AutoSave 与手动 SaveToFile 两种模式类默认提供 AutoSave 属性设为 True 时解析完自动调用每个文件的 SaveToFile 方法。如果你需要更灵活的控制比如要把文件存到数据库、或者需要先判断再保存也可以设 AutoSaveFalse然后自己遍历文件集合手动调用objFile.SaveToFile(strPath)。我实际用下来的经验是绝大多数场景 AutoSave 就够了手动控制反而容易漏存文件导致数据一致性问题。只有当你需要把文件名、大小这些信息先写入数据库再决定文件存不存的时候才需要关闭 AutoSave。这种场景下记得事务处理数据库写入失败时把已保存的文件也删掉别让服务器上留一堆没用的残废文件。6. 无组件上传最常见的坑从 IIS 版本差异到中文文件名6.1 中文文件名的编码问题GBK 还是 UTF-8这是让无数 ASP 开发者抓狂的问题。客户端浏览器在发送文件名时会根据页面的字符集来决定编码。如果上传页面是 GBK/GB2312 编码文件名用 GBK 编码传输如果页面是 UTF-8文件名用 UTF-8 传输。而 ASP 的服务端在取出这些字节之后默认按什么编码解析呢这取决于服务器系统区域和 ASP 脚本代码页。AienAspUpload 的做法是同时兼容两种编码先尝试按 UTF-8 解码如果结果看起来是乱码再按 GBK 解码。这个“看起来是乱码”的判断不够优雅但实践中效果还行。更靠谱的方法是读请求里修订后的值或者从字符串里寻找常见的文件名特征字符。遇到中文名上传后存成乱码的多半是编码判断走了错误分支。更省心的做法是服务端根本不用客户端文件名而是像我上面说的自己生成随机文件名。这样即使中文名乱码不影响文件保存顶多丢失原来的文件名信息。需要保留原文件名的业务场景我建议把原文件名重新编码后存进数据库用 URL 编码如encodeURIComponent在客户端处理服务端解码后入库。6.2 文件大小限制的几个“隐形关卡”很多用户反映“传 3MB 就报错”怀疑是类的 MaxSize 属性设置的太小。其实要检查的是三个层面第一层AspMaxRequestEntityAllowed这是 IIS 的全局上传限制默认 200KB 左右超过就报 404.13 或 413 错误第二层Request.BinaryRead的大小限制在 IIS 7 以上默认只读取约 30 万字节具体跟配置有关需要在web.config中调整第三层类的MaxSize属性这个是业务层校验到了这一层说明服务器已经接受了请求体只是我们自己不允许太大。还有一个容易被忽视的地方ASP 脚本超时时间。如果一个文件上传很慢比如用户通过低速网络上传 20MB 文件到 90 秒默认超时的时候脚本会被 IIS 强制终止文件保存一半就断了。处理办法是在文件开头设置Server.ScriptTimeout 300这个时间要结合你的实际业务来定太大容易被别人挂一堆大文件拖住进程太小又导致慢速上传失败。6.3 服务器权限问题ASP 写不了目录这不算无组件上传独有的坑但无组件方案里因为不装组件、没有更底层的读写通道权限问题反而更常见。IIS 的匿名访问用户通常是 IUSR 或 IIS_IUSRS对保存目录必须要有写权限。我曾经遇到过客户把目录设在系统盘 Program Files 下面权限全是拒绝访问上传时 ADODB.Stream 的 SaveToFile 报“没有权限”。解决办法是对保存目录单独设置写权限右键目录 - 属性 - 安全 - 添加 IUSR 用户 - 勾选“修改”和“写入”。注意不要给“完全控制”只要写入和修改就够了给得多了反而有安全隐患。6.4 安全加固不能只防服务器崩溃还得防攻击者无组件上传类因为代码公开即使你不使用别人的自己写也容易漏掉安全校验。AienAspUpload 里做了三层防御第一层是扩展名白名单只允许类的AllowExt属性里列出的扩展名。这个白名单机制比黑名单安全得多黑名单永远列不全今天漏了.asp明天可能有.cer、.asa、.cdx这些 IIS 能当脚本执行的扩展名。白名单一栏不在名单里的一律拒绝。第二层是 MIME 头校验。实际上根据 Content-Type 判断文件类型并不可靠因为客户端可以伪造。但我会对客户端提交的 Content-Type 做一层粗校验如果明显和扩展名不匹配比如 Content-Type 是 image/jpeg 但扩展名是 .asp直接拒收。这层只是辅助防御主要防无聊的扫描器。第三层是路径穿越防护。某些老代码里如果允许用户自定义保存路径攻击者通过../序列可以把文件写到网站根目录外甚至系统目录配合脚本执行条件就会造成比较严重的漏洞。AienAspUpload 保存文件名不用用户输入SavePath 里如果有..也会过滤掉保证文件只能在限定目录范围内。7. 再聊聊无组件上传的边界适合什么不适合什么做一个上传类做了这么多年我越来越清楚它的能力边界。AienAspUpload 这类无组件方案最适合什么场景呢部署在虚拟主机、受限服务器上上传单个文件不超过 10MB并发量不高业务逻辑不复杂的传统 ASP 网站。比如企业官网的图片新闻、老系统的附件管理、后台的图片上传维护。它能以几乎为零的部署成本解决刚需省掉的组件授权费和部署工时很可观。但它不适合的场景也很明确超大文件上传比如视频、几百MB的日志、高并发上传比如 OA 系统里百人同时传附件、需要断点续传的场景。这些情况本质上是协议层的能力问题纯 ASP 解析整段流的方式无论如何都做不了。遇到这种需求我更推荐换技术栈或者至少上 IIS 的官方扩展模块、专用的上传服务组件别硬套无组件方案。我也看到过有人用“分块上传”的网页关键字来搜无组件上传这其实是另一种思路把大文件在客户端用 JavaScript比如 File.slice切成多个小段每段用普通表单提交、一次传一段服务端接收后用同名参数标识块序号全部传完后服务端再做合并。这种方式可以绕过单次请求大小限制也用不着组件但实现复杂度提高了不少需要客户端逻辑和服务端逻辑配合。AienAspUpload 早期的版本不含分块合并功能但类的设计里预留了按块接收的接口如果你确实需要分块上传可以在现有类的 SaveToFile 基础上扩展一个“追加写模式”每块到达后不是覆盖写入而是移动到文件末尾。这个扩展思路我给很多做老系统维护的朋友提过反馈还不错。最后分享一个小技巧用无组件上传保存文件时尽量把保存路径和生成文件名都放到配置区或者函数入口方便迁移文件保存成功以后立即用 FileSystemObject 检查一下文件大小是否和类的FileSize属性一致不一致就删掉能挡掉一部分网络异常导致的半截文件。这个习惯我保持了十几年几乎变成了肌肉记忆。无组件上传在今天的 Web 开发里确实算“上古技术”了但在那些你还甩不掉的 ASP 老项目里它依然是一根能救命的稻草。希望这篇拆解能让你少踩几个我当年踩过的坑把这根稻草用得更顺手。本文还有配套的精品资源点击获取
返回列表