简介:艾恩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.0,Windows 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 action="upload.asp" method="post" enctype="multipart/form-data"> <input type="text" name="username" /> <input type="file" name="myfile" /> <input type="submit" /> </form>注意那个enctype="multipart/form-data",这是文件上传的关键。普通表单默认的application/x-www-form-urlencoded是简单文本拼接,而文件上传必须用 multipart 格式,因为文件内容是一堆二进制字节,跟普通字符混在一起没法用 URL 编码处理。
当用户点了提交,浏览器发出的 HTTP 请求体长这样(我简化了头部,但格式一致):
-----------------------------7da1faf201fc Content-Disposition: form-data; name="username" 张三 -----------------------------7da1faf201fc Content-Disposition: form-data; name="myfile"; filename="test.txt" Content-Type: text/plain <这里是文件test.txt的原始内容...> -----------------------------7da1faf201fc--每一段内容之间用一个 boundary(边界字符串)分隔,整个请求体以--boundary开头、以--boundary--结尾。浏览器每次生成的 boundary 是一串随机字符,所以服务端不能写死,必须从请求头里取。
2.2 服务端拿到的是什么:Byte 数组,不是文本
ASP 的 Request.BinaryRead 返回的是什么呢?是一个二进制字节数组,也就是说请求体里所有内容——文本、数字、文件名、文件内容——全部以字节形式混在同一个数组里。你在浏览器里看到“张三”这两个字,在数组里可能是0xD5 0xC5 0xC8 0xFD(GBK 编码)或者0xE5 0xBC 0xA0 0xE4 0xB8 0x89(UTF-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:文件字节数FileType:Content-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.webServer><security><requestFiltering><requestLimits>节点里改;如果用 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.Stream:Type=1 意味着什么
解析出文件内容字节数组以后,还不能直接用 FSO 写文件。因为 FSO 的 CreateTextFile 是按文本模式写的,文件字节流里可能有0x00、0xFF这些非可见字符,经过 FSO 处理就会出错。正确做法是用 ADODB.Stream,打开后先把 Type 设为 1(adTypeBinary),再写入:
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 action="upload.asp" method="post" enctype="multipart/form-data" name="form1"> 用户名: <input type="text" name="username" /><br /> 选择文件: <input type="file" name="myfile" /><br /> <input type="submit" value="上传" /> </form>这里必须强调:method必须是post,enctype必须是multipart/form-data,这两个属性缺一不可。有人把 enctype 写成multipart/form-data的时候好奇心重,在后面加空格或者大小写不一致,浏览器发送的 Content-Type 里 boundary 就会出问题,服务端解析就会失败。实际测试中遇到过用户把属性写成multipart/form-data; charset=utf-8的,这种写法在服务端提取 boundary 时会多出东西,需要类内部做容错处理。AienAspUpload 在提取 boundary 时会把分号也作为边界处理,比较稳妥。
5.2 接收端 upload.asp 的核心调用代码
接收端代码用起来很简单:
<!-- #include file="AienAspUpload.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 方法。如果你需要更灵活的控制,比如要把文件存到数据库、或者需要先判断再保存,也可以设 AutoSave=False,然后自己遍历文件集合手动调用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 老项目里,它依然是一根能救命的稻草。希望这篇拆解能让你少踩几个我当年踩过的坑,把这根稻草用得更顺手。
本文还有配套的精品资源,点击获取