简介:艾恩ASP无组件上传类AienAspUpload是一套面向ASP开发者的轻量级文件上传解决方案,适合需要在传统ASP环境中快速实现表单处理与文件上传的初中级开发者。该类以简洁易懂的代码实现核心上传逻辑,支持提取表单数据、上传至不同文件夹、上传同时保存到数据库、限制上传扩展名与大小,以及按原文件名或时间随机命名保存等实用功能。压缩包共50个文件,约331KB,以asp源码为主体,辅以png、jpg、gif等图片资源、js脚本、swf组件及pdf说明文档,涵盖muti、iframe、form、list、single、complex、swfupload等多种上传场景的示例页面,便于对照不同模式理解实现思路。该版本为v13.11.25 build2013.11.22,修复了恶意构造上传可能导致死循环的BUG。目前已有317人学习,读者可获取完整类文件、多场景演示代码与帮助文档,快速掌握无组件上传的排错与集成方法。
1. 老 ASP 项目还在跑,上传功能却卡在组件装不上
接手一个维护了十几年的老系统是什么体验?服务器上 IIS 还在转,Access 数据库还能读,但一到文件上传就报错——因为当年用的第三方上传组件在新服务器上根本注册不了。这种场景在存量 ASP 项目里太常见了:系统不能重写,预算不支持迁移,但业务就是需要传图片、传附件。艾恩ASP无组件上传类(AienAspUpload)就是为这种局面准备的——它不依赖任何 COM 组件注册,纯 ASP 脚本实现文件上传解析,把multipart/form-data请求体拆开、提取文件流、写到磁盘。适合两类人:一是维护老 ASP 系统的开发者,二是需要在受限服务器环境下快速补上传能力的实施人员。核心价值就一句话:不装组件、不改服务器配置,把上传跑通。
2. 无组件上传到底怎么把文件从请求体里抠出来
2.1 为什么传统方案必须装组件,而无组件方案能绕开
ASP 原生没有处理文件上传的内置对象。传统做法是调用第三方 COM 组件(比如某些常见的上传组件),由组件在服务器端解析二进制请求流。问题在于:组件需要注册到系统、需要对应位数(32/64 位)匹配、需要管理员权限,很多虚拟主机或云服务器根本不给注册权限。
无组件方案的核心思路完全不同:既然 ASP 能通过Request.BinaryRead拿到原始请求体字节流,那就在脚本层面自己解析multipart/form-data的格式。这个格式其实不复杂——它用一段 boundary 字符串分隔每个字段,文件字段里包含文件名、Content-Type 和二进制内容。艾恩ASP无组件上传类做的就是这件事:读字节流、找 boundary、切出文件段、写入文件。
代价是什么?纯脚本解析二进制在 ASP 里效率不高,大文件会吃内存,而且 VBScript 处理字节数组的能力有限。所以这类方案通常限制单文件在几 MB 到几十 MB 之间,适合图片、文档等中小文件场景。
2.2 multipart/form-data 请求体的结构拆解
要理解上传类的工作方式,得先看清浏览器发过来的请求体长什么样。假设表单里有一个文本字段username和一个文件字段photo,请求体大致是:
------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="username" zhangsan ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="photo"; filename="test.jpg" Content-Type: image/jpeg (这里是 JPEG 二进制数据) ------WebKitFormBoundaryABC123--关键点有三个:
- boundary 是分隔符:每段以
--boundary开头,最后一段以--boundary--结束。boundary 的值在请求头Content-Type: multipart/form-data; boundary=xxx里。 - 文件段的头部有元信息:
filename是原始文件名,Content-Type是 MIME 类型。这两个值决定了后续怎么存文件、怎么校验。 - 文件内容紧跟空行之后:从头部结束后的第一个空行开始,到下一个 boundary 之前,全是二进制数据。注意,这段数据里可能包含
0x0D 0x0A(回车换行),不能简单按文本行来切。
艾恩ASP无组件上传类的解析逻辑就是围绕这三步走的:先定位 boundary,再逐段扫描,遇到文件段就提取二进制写入磁盘。
2.3 用 AienAspUpload 跑通第一个上传的最小步骤
下面是一个最小可用的上传页面。假设你已经把AienAspUpload.asp类文件放在站点目录下。
<!-- upload.asp --> <%@ Language="VBScript" CodePage="65001" %> <% ' 引入上传类文件 <!--#include file="AienAspUpload.asp"--> Dim uploader, savePath, maxSize, allowExt Set uploader = New AienAspUpload ' 设置上传参数 savePath = Server.MapPath("/uploads/") ' 文件保存目录,必须是物理路径 maxSize = 5 * 1024 * 1024 ' 单文件最大 5MB allowExt = "jpg|jpeg|png|gif|pdf|doc|docx" ' 允许的扩展名,用竖线分隔 ' 执行上传解析 uploader.SavePath = savePath uploader.MaxSize = maxSize uploader.AllowExt = allowExt uploader.SaveFile ' 触发解析和保存 ' 获取上传结果 If uploader.ErrCode = 0 Then Response.Write "上传成功:" & uploader.FileName Else Response.Write "上传失败,错误码:" & uploader.ErrCode & ",说明:" & uploader.ErrMsg End If Set uploader = Nothing %>对应的表单页面:
<!-- form.html --> <form action="upload.asp" method="post" enctype="multipart/form-data"> <input type="text" name="username" /> <input type="file" name="photo" /> <input type="submit" value="上传" /> </form>逻辑说明:SaveFile方法内部会调用Request.BinaryRead读取整个请求体,然后按 boundary 切分。文本字段会被解析到类的属性里(比如uploader.Form("username")),文件字段则根据SavePath写入磁盘。ErrCode为 0 表示成功,非 0 时ErrMsg给出具体原因。
参数说明:
SavePath:必须是服务器物理路径,用Server.MapPath转换。目录必须存在且 IIS 用户有写权限。MaxSize:单位是字节。超过这个值的文件会被拒绝,不会写入磁盘。AllowExt:扩展名白名单,用|分隔。不在列表里的文件会被拒绝。注意这个校验只看扩展名,不检查文件内容。
2.4 关键参数怎么设:目录权限、大小限制、编码
目录权限是最容易翻车的地方。IIS 的匿名用户(通常是IUSR或IIS_IUSRS)需要对上传目录有「写」权限。在 Windows 资源管理器里右键目录 → 属性 → 安全 → 编辑 → 添加IIS_IUSRS→ 勾选「写入」。如果用的是虚拟主机,通常需要在控制面板里把目录权限设为 777 或可写。
大小限制要同时看三个地方:
| 限制位置 | 默认值 | 修改方式 |
|---|---|---|
| 上传类 MaxSize | 类默认值 | 代码里设置 |
| IIS 请求体限制 | 约 200KB(旧版) | web.config里<requestLimits maxAllowedContentLength="52428800" /> |
| ASP 缓冲区限制 | 约 100KB | 注册表或AspMaxRequestEntityAllowed元数据库 |
很多人只改了上传类的MaxSize,结果大文件还是传不上去,就是因为 IIS 层面先拦了。常见做法是先在web.config里把maxAllowedContentLength调到 50MB,再确认 ASP 的请求体限制。
编码问题:如果文件名包含中文,需要在保存前做编码转换。艾恩ASP无组件上传类通常会把原始文件名放在FileName属性里,但浏览器传过来的可能是 UTF-8 编码的字节。如果服务器是 GB2312 环境,直接保存会乱码。解决办法是在保存前用ADODB.Stream做一次编码转换,或者手动把文件名从 UTF-8 转成 GB2312。
3. 从请求体到磁盘文件:解析流程与代码实现
3.1 读取二进制流并定位 boundary 的代码逻辑
上传类最核心的一步是拿到 boundary 并找到第一段数据的起始位置。下面是一段简化后的逻辑示意:
' 获取 Content-Type 头,提取 boundary Dim contentType, boundaryPos, boundary contentType = Request.ServerVariables("HTTP_CONTENT_TYPE") boundaryPos = InStr(contentType, "boundary=") If boundaryPos > 0 Then boundary = Mid(contentType, boundaryPos + 9) ' 实际 boundary 前面还有两个连字符 boundary = "--" & boundary End If ' 读取整个请求体 Dim binData, totalSize totalSize = Request.TotalBytes binData = Request.BinaryRead(totalSize) ' 用 ADODB.Stream 把字节流转成可操作的格式 Dim stm Set stm = Server.CreateObject("ADODB.Stream") stm.Type = 1 ' 二进制模式 stm.Open stm.Write binData stm.Position = 0逻辑说明:Request.BinaryRead一次性把请求体读进binData。但 VBScript 对字节数组的操作能力很弱,所以通常借助ADODB.Stream来做字节级别的查找和截取。stm.Type = 1表示二进制模式,stm.Position用来移动读取指针。
参数说明:Request.TotalBytes是请求体的总字节数,必须作为BinaryRead的参数传入。如果请求体超过服务器限制,这一步就会报错。
3.2 切分文件段并写入磁盘的完整流程
拿到二进制流之后,需要按 boundary 切分。简化流程如下:
' 将二进制流转为 Latin-1 字符串,便于用 InStr 查找 boundary Dim binStr stm.Position = 0 binStr = stm.ReadText(-1) ' 以文本形式读出,每个字节对应一个字符 ' 查找第一段和文件段的起始位置 Dim firstBoundary, fileStart, fileEnd firstBoundary = InStr(binStr, boundary) ' 查找文件头中的 filename Dim headerEnd, headerStr headerEnd = InStr(firstBoundary, binStr, vbCrLf & vbCrLf) headerStr = Mid(binStr, firstBoundary, headerEnd - firstBoundary) ' 从头部提取文件名 Dim fileName, fnPos fnPos = InStr(headerStr, "filename=") If fnPos > 0 Then fileName = Mid(headerStr, fnPos + 10) fileName = Replace(fileName, """", "") fileName = Left(fileName, InStr(fileName, vbCrLf) - 1) End If ' 文件内容从头部结束后的空行之后开始 fileStart = headerEnd + 4 ' 文件内容到下一个 boundary 之前结束 fileEnd = InStr(fileStart, binStr, boundary) - 2 ' 截取文件二进制并写入磁盘 Dim fileData fileData = Mid(binStr, fileStart, fileEnd - fileStart) Dim outStm Set outStm = Server.CreateObject("ADODB.Stream") outStm.Type = 1 outStm.Open outStm.Write TextToBinary(fileData) ' 需要把字符串转回二进制 outStm.SaveToFile savePath & fileName, 2 ' 2 表示覆盖 outStm.Close逻辑说明:这里用了一个常见技巧——把二进制流转成 Latin-1 字符串(每个字节映射到一个字符),这样就可以用InStr和Mid来查找和截取。找到文件内容后,再转回二进制写入磁盘。SaveToFile的第二个参数2表示如果文件存在就覆盖。
参数说明:vbCrLf & vbCrLf是头部和内容之间的空行分隔符。fileEnd减 2 是因为 boundary 前面有一个vbCrLf,不属于文件内容。
3.3 文件类型校验:扩展名白名单与 MIME 检查
只靠扩展名白名单是不够的——攻击者可以把.asp文件改成.jpg上传,如果服务器把.jpg当 ASP 解析就完了。所以需要两层校验:
' 第一层:扩展名白名单 Dim ext, allowList ext = LCase(Mid(fileName, InStrRev(fileName, ".") + 1)) allowList = Split("jpg|jpeg|png|gif|pdf|doc|docx", "|") Dim isAllowed, i isAllowed = False For i = 0 To UBound(allowList) If ext = allowList(i) Then isAllowed = True Exit For End If Next ' 第二层:检查文件头魔数 Dim magic, isImage magic = LeftB(fileData, 4) ' 取前 4 个字节 ' JPEG 以 FF D8 FF 开头,PNG 以 89 50 4E 47 开头 isImage = (AscB(MidB(magic, 1, 1)) = &HFF And AscB(MidB(magic, 2, 1)) = &HD8) _ Or (AscB(MidB(magic, 1, 1)) = &H89 And AscB(MidB(magic, 2, 1)) = &H50)逻辑说明:扩展名校验是第一道门,文件头校验是第二道门。JPEG 文件的前两个字节固定是FF D8,PNG 的前两个字节是89 50。如果扩展名是.jpg但文件头不是FF D8,说明文件被伪造了。
参数说明:AscB和MidB是 VBScript 里处理字节的函数,AscB返回字节的十进制值,&HFF是十六进制表示。
3.4 上传目录的安全配置:为什么不能给执行权限
上传目录必须禁止脚本执行。在 IIS 里可以这样设置:选中上传目录 → 处理程序映射 → 编辑功能权限 → 取消「脚本」和「执行」权限。这样即使有人上传了.asp文件,IIS 也不会执行它,只会当静态文件返回或直接拒绝。
另一个常见做法是把上传目录放在网站根目录之外,通过一个专门的 ASP 页面来读取和输出文件。这样文件根本不在 Web 可访问路径下,安全性更高。
4. 避坑与排查:那些让你加班到凌晨的上传问题
4.1 上传大文件时报错 “Request 对象 错误 ASP 0104 : 80004005”
现象:上传超过 200KB 的文件时,页面报ASP 0104 : 80004005,小文件正常。
原因:IIS 默认限制请求体大小。旧版 IIS 的AspMaxRequestEntityAllowed默认约 200KB,超过就拒绝。
解决:在web.config里添加:
<configuration> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="52428800" /> </requestFiltering> </security> </system.webServer> </configuration>同时确认 ASP 的AspMaxRequestEntityAllowed也调大了。如果服务器不允许改配置,只能在前端限制文件大小。
4.2 中文文件名变成乱码或问号
现象:上传测试图片.jpg,保存到磁盘后文件名变成?????.jpg或乱码。
原因:浏览器发送的 filename 是 UTF-8 编码,而服务器默认用 GB2312 解析。编码不匹配导致乱码。
解决:在保存前做编码转换。常见做法是用ADODB.Stream把 UTF-8 字节流转成 GB2312 字符串:
Function UTF8ToGB2312(str) Dim stm1, stm2 Set stm1 = Server.CreateObject("ADODB.Stream") stm1.Type = 2 stm1.Charset = "UTF-8" stm1.Open stm1.WriteText str stm1.Position = 0 stm1.Charset = "GB2312" UTF8ToGB2312 = stm1.ReadText stm1.Close End Function或者更简单:保存时用时间戳+随机数重命名文件,原始文件名只存数据库,不用于磁盘路径。
4.3 上传成功但文件大小为 0 字节
现象:页面提示上传成功,但磁盘上的文件是空的。
原因:通常是fileEnd计算错误,导致截取的二进制长度为 0。常见于 boundary 匹配到了错误的位置,或者headerEnd定位到了文件内容里的空行而不是头部结束的空行。
解决:在切分时打印调试信息,确认firstBoundary、headerEnd、fileStart、fileEnd四个值是否合理。特别注意:文件内容里可能包含vbCrLf & vbCrLf,所以headerEnd必须从firstBoundary之后开始找,不能从整个字符串开头找。
4.4 上传目录权限不足导致 “Permission denied”
现象:代码逻辑没问题,但SaveToFile时报权限错误。
原因:IIS 匿名用户对上传目录没有写权限。
解决:右键上传目录 → 属性 → 安全 → 编辑 → 添加IIS_IUSRS→ 勾选「写入」和「修改」。如果是虚拟主机,在控制面板里把目录权限设为可写。注意:不要给整个网站目录写权限,只给上传目录。
4.5 上传后的文件被当作 ASP 执行
现象:有人上传了一个.asp文件,然后通过 URL 访问,服务器执行了它。
原因:上传目录没有禁止脚本执行,IIS 把.asp文件当脚本处理了。
解决:立即在上传目录的处理程序映射里取消「脚本」和「执行」权限。同时检查上传类的扩展名白名单,确保.asp、.asa、.cer、.cdx等危险扩展名不在列表里。更彻底的做法是把上传目录移出网站根目录。
5. 进阶技巧:让无组件上传在真实项目里更稳
5.1 用时间戳+随机数重命名,避免文件名冲突和注入
原始文件名不可信——可能包含路径分隔符、特殊字符、超长字符串。我一般会这样处理:
' 生成安全文件名:时间戳 + 随机数 + 原扩展名 Dim safeName, ext ext = LCase(Mid(fileName, InStrRev(fileName, ".") + 1)) Randomize safeName = Year(Now) & Right("0" & Month(Now), 2) & Right("0" & Day(Now), 2) & _ Right("0" & Hour(Now), 2) & Right("0" & Minute(Now), 2) & Right("0" & Second(Now), 2) & _ "_" & Int(Rnd * 9000 + 1000) & "." & ext这样生成的文件名类似20250115143022_4821.jpg,既不会冲突,也不会有注入风险。原始文件名存到数据库里,展示时再读出来。
5.2 多文件上传的处理:循环解析每个文件段
艾恩ASP无组件上传类通常支持多文件字段。处理方式是:在解析完一个文件段后,继续从fileEnd之后查找下一个 boundary,直到遇到结束标记--boundary--。代码结构大致是:
Dim pos, fileCount pos = 1 fileCount = 0 Do While True Dim bPos bPos = InStr(pos, binStr, boundary) If bPos = 0 Then Exit Do ' 检查是否是结束标记 If Mid(binStr, bPos + Len(boundary), 2) = "--" Then Exit Do ' 解析这一段 ' ... fileCount = fileCount + 1 pos = bPos + Len(boundary) Loop注意每次循环后要把pos移到当前 boundary 之后,避免死循环。
5.3 上传进度反馈的替代方案
纯 ASP 无组件上传没法做真正的进度条——因为BinaryRead是一次性阻塞读取,服务器在读完之前不会返回任何响应。常见的替代方案是:
- 前端用 AJAX + 隐藏 iframe:提交表单到 iframe,用 JavaScript 轮询一个记录上传状态的接口。但这个接口只能在文件保存完成后才更新状态,所以进度是「假」的。
- 用 Flash 或 Silverlight:这些技术已经淘汰,不推荐。
- 分片上传:前端把文件切成小块,逐块上传,服务端合并。这个方案在 ASP 里实现复杂度较高,但能做到真实进度。如果项目对进度有硬需求,建议评估是否值得投入。
我个人的经验是:对于中小文件(10MB 以内),进度条不是刚需,给个「上传中,请稍候」的提示就够了。把精力花在文件校验和目录安全上更划算。
5.4 一个容易忽略的细节:表单 enctype 必须写对
最后说一个血泪教训。有次排查一个上传问题,代码翻来覆去看没问题,最后发现是表单的enctype写成了application/x-www-form-urlencoded。这个属性不写对,浏览器根本不会发multipart/form-data请求体,上传类自然拿不到文件。所以每次新建上传页面,第一件事就是检查<form>标签的enctype属性。另外,method必须是post,get请求没有请求体,上传无从谈起。
希望帮到你。
本文还有配套的精品资源,点击获取