本章主干知识点:

  • 服务器返回的状态码 200、302、404、500 分别是什么意思?
  • GET 和 POST 的区别
  • server.MapPath 的作用
  • ContentType 的作用
  • ASP.NET 处理文件上传
  • ASP.NET 中对于用户上传的文件要做哪些检查

HTTP 协议

HTTP(超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议,它是万维网数据通信的基础。

  1. 连接(Connection):浏览器和服务器之间传输数据的通道(一般就是个 socket 通道)。 一般请求完毕就关闭,HTTP 不保持连接。不保持连接会降低处理速度(因为建立连接速度很慢),保持连接会降低处理客户端的并发请求数,而不保持连接服务器可以处理更多的请求
    1. 可以通过 Keep-Alive、Http/2 等实现保持连接
  2. 请求(Request):浏览器向服务器发送“我要 xxx”的消息,包含请求的类型、请求的数据、浏览器的信息(语言、浏览器版本等)
  3. 响应(Response):服务器对浏览器的请求返回的数据,包含是否成功、错误码等
    1. 浏览器不知道服务器内部发生了什么,也不知道服务器是直接输出静态文件还是经过 C# 运算动态输出
  4. 处理(Process)

    HTTP 协议报文

页面图片请求的 Referer:
image.png
表示对这张图片的请求来自于 login.ashx 页面。
Refer 可以用来判断用户是从那个页面来到当前页面的。

image.png

Request Headers:

  • GET / HTTP/1.1:用 GET 方式向服务器请求首页,使用 HTTP/1.1 协议
  • User-Agent(UA):浏览器版本信息。通过该信息可以读取浏览器是 IE 还是 FireFox、支持的插件、.NET 版本等
  • Referer:请求的来源页面、所属页面
  • Accept-Encoding:服务器支持什么压缩算法
  • Accept-Language:浏览器支持什么语言

注:浏览器请求是可以伪造的,不能信任。

image.png

Response Headers:

  • Content-Type: 返回数据类型
    • charset:报文体编码格式
  • Accept-Ranges:服务器是否支持断点续传
  • Server:服务器版本
  • X-Powered-By:服务器端语言
  • Content-Length:正文字节数

注:响应也是可以造假的,有的网站通过修改 X-Powered-By 和 Server 来隐藏服务器信息以欺骗黑客。

响应码

  • 200:OK
  • 302:Found 暂时转移,用于重定向。Response.Redirect() 让浏览器以 GET 方式再请求一次重定向的地址
  • 304:服务器把文件的修改日期通过 Last-Modified 返回给浏览器,浏览器缓存该文件。当下次向服务器请求该文件时,通过 If-Modified-Since 问服务器“我本地的文件的修改日期是……”,服务器如果发现文件没有修改,就返回 304 Not Modified,浏览器继续使用本地缓存
    • 可通过 Ctrl + F5 强制刷新
  • 403:客户端访问未被授权。
  • 404:Not Found
  • 500:服务器错误(一般服务器出现异常),通过报错信息找出异常的点


    总结:2xx 没问题;3xx 浏览器需要干点啥;4xx 浏览器错误;5xx 服务器错误

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

重定向示例:
将页面重定向至 1.html
image.png

Size 谜题

  • Login1.html 在本地是 854 B
  • 浏览器第一次请求 Size 是 1.2K
  • 浏览器之后(有缓存)请求 Size 只有 295B


    注:Login1.html 是个单纯的 HTML 文件,内部不包含图片等页面资源,但浏览器依然进行了缓存,由此看出浏览器不仅缓存页面资源也缓存网页源码。

SOF 上关于该问题的解析:
“Size” is the number of bytes on the wire, and “content” is the actual size of the resource. A number of things can make them different, including:

  • Being served from cache (small or 0 “size”)
  • Response headers, including cookies (larger “size” than “content”)
  • Redirects or authentication requests
  • gzip compression (smaller “size” than “content”, usually)

    GET 与 POST


    区别:

  • GET(默认值)通过 URL 传递表单值,POST 传递的表单值隐藏到 HTTP 报文体中

  • GET 传递的数据量有限、且明文传递不适合传递密码


    POST 注意点:

  • 无法把网址发给其他人

  • 若原来是 POST,F5 刷新依然是 POST,但在地址栏回车会变成 GET


    URL 中的汉字、特殊符号等会被编码。

GET 通过 URL 传值:
image.png

POST 通过报文头传值:
image.png

HttpContext


HttpContext:和本次请求相关对象的一个上下文对象,一般通过它获取其他对象。

在 HttpHandler 的 ProcessRequest 方法中可以通过方法的 context 参数获得该对象。

在其他地方可以通过 HttpContext.Current 拿到当前请求堆栈中的 HttpContext 对象,但是建议通过参数传递,这样思路清晰,避免对全局对象的依赖。

注:在子线程是无法获得 HttpContext.Current
推荐通过参数传递 HttpContext:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. ...
  4. Test(context);
  5. context.Response.Write("</body></html>");
  6. }
  7. void Test(HttpContext context)
  8. {
  9. if (!string.IsNullOrEmpty(context.Request["wonder"]))
  10. {
  11. context.Response.Write("梦想");
  12. }
  13. }

HttpRequest


context.Request(HttpRequest 类型),包含请求相关的信息。

  • context.Request.Form[“name”]:获取 POST 请求中的值
  • context.Request.QueryString[“name”]:获取 GET 请求中的值
  • context.Request[“name”]:依次从 QueryString、Form、Cookies、 ServerVariables 中找,第一个找到的就是(下面通过反编译进行了验证)


    注:所有 HttpRequest 的信息都来自于浏览器当初发送请求的 Request Headers。服务器无法获知任何浏览器未提交的信息。

通过 JetBrains dotPeek 反编译 HttpRequest 的索引器:

  1. public string this[string key]
  2. {
  3. get
  4. {
  5. string str1 = this.QueryString[key];
  6. if (str1 != null)
  7. return str1;
  8. string str2 = this.Form[key];
  9. if (str2 != null)
  10. return str2;
  11. HttpCookie cookie = this.Cookies[key];
  12. if (cookie != null)
  13. return cookie.Value;
  14. return this.ServerVariables[key] ?? (string)null;
  15. }
  16. }

HttpRequest 示例:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. // Brower 还有很多属性
  5. context.Response.Write(context.Request.Browser.Browser + "\n");
  6. context.Response.Write(context.Request.Browser.Platform + "\n");
  7. context.Response.Write(context.Request.Browser.Version + "\n");
  8. context.Response.Write("--------------\n");
  9. foreach (var key in context.Request.Headers.AllKeys)
  10. {
  11. context.Response.Write(key + ":" + context.Request.Headers[key] + "\n");
  12. }
  13. context.Response.Write("--------------\n");
  14. context.Response.Write(context.Request.HttpMethod+ "\n");
  15. context.Response.Write(context.Request.InputStream + "\n");
  16. context.Response.Write(context.Request.Path + "\n");
  17. context.Response.Write(context.Request.QueryString + "\n");
  18. context.Response.Write(context.Request.PhysicalPath+ "\n");
  19. context.Response.Write(context.Request.UserAgent + "\n");
  20. // 客户端 IP 地址
  21. context.Response.Write(context.Request.UserHostAddress + "\n");
  22. context.Response.Write(context.Request.UrlReferrer + "\n");
  23. context.Response.Write(context.Request.UserLanguages + "\n");
  24. }

image.png

HttpResponse


context.Response,包含响应相关信息。

  • ContentType:返回数据类型
  • OutputStream:输出流
  • End():将当前所有缓冲的输出发送到客户端,停止该页的执行
  • Redirect():重定向

通过对 End() 进行异常捕获,发现是抛出了 ThreadAbortException,所以 End() 之后的代码就不会执行了。
image.png

因为异常处理效率低,所以能用 return 时就不用 End():

  1. if (string.IsNullOrEmpty(name))
  2. {
  3. context.Response.Write("用户名为空");
  4. context.Response.End();
  5. }
  6. if (string.IsNullOrEmpty(pwd))
  7. {
  8. context.Response.Write("密码为空");
  9. return;
  10. }

context.Server


Server 是一个 HttpServerUtility 类型的对象

  • MapPath:将虚拟路径(~代表项目根目录)转换为磁盘上的绝对路径,操作项目中的文件时常用
  • HtmlEncode、HtmlDecode:HTML 编码解码
  • UrlEncode、UrlDecode:URL 编码解码。汉字、特殊字符(空格、尖括号)等通过 URL 传递时需编码
    • URL 传输前最好进行 UrlEncode 编码
  • Transfer()

MapPath:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. // 绝对路径,一旦项目移动到其它位置就失效了;但程序中也不提倡使用相对路径,容易进坑
  5. //var fi = new FileInfo(@"F:\Projects\ASP.NETCoreDemo\Web1\海错图.jpg");
  6. // 转换相对路径为绝对路径
  7. var imgPath = context.Server.MapPath("~/海错图.jpg");
  8. var fi = new FileInfo(imgPath);
  9. context.Response.Write(fi.Length);
  10. }

HtmlEncode:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var csCode = "var list = new List<T>()";
  5. // 把 < > 等特殊字符转换为 HTML 转义字符
  6. var encodeCsCode = context.Server.HtmlEncode(csCode);
  7. context.Response.Write(encodeCsCode);
  8. }

HtmlEncode 的效果:
image.png

HtmlDecode:

  1. var encoded = "var list = new List&lt;T&gt;();";
  2. var source = context.Server.HtmlDecode(encoded);

UrlEncode:
“香香”被 URL encode 为 “%E9%A6%99%E9%A6%99”
image.png

为什么需要进行 URL Encode

  1. URL 表示字符序列而不是八位字节序列。这是因为 URL 可能通过非计算机网络传输,例如被打印在纸上、通过收音机播放
  2. 将包含 non-ASCII 字符的原始序列转换为 ASCII 字符序列

输出图片

HttpHandler 是对请求的响应,既可以输出 HTML 内容,也可以输出图片、输出文件供下载。

注:

  1. 永远不要使用中文文件名
  2. 再次强调,浏览器不知道服务器上是有原始图片文件还是动态生成的图片

输出已有图片:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. // image/gif image/png
  4. context.Response.ContentType = "image/jpeg";
  5. var filePath = context.Server.MapPath("~/hct.jpg");
  6. // 浏览器不知道服务器上有 htc.jpg 存在
  7. // 浏览器只发请求,接收请求,别的一概不知
  8. using (Stream inStream = File.OpenRead(filePath))
  9. {
  10. inStream.CopyTo(context.Response.OutputStream);
  11. }
  12. }

动态生成字符串图片:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "image/jpeg";
  4. var printStr = context.Request["string"];
  5. using (var bmp = new Bitmap(300, 300))// 创建一个尺寸为 300*300 的内存图片
  6. using (var g = Graphics.FromImage(bmp))// 得到图片的画布
  7. using (var font = new Font(FontFamily.GenericSerif, 30))
  8. {
  9. // 设置背景为白色
  10. g.Clear(Color.White);
  11. g.DrawString(printStr, font, Brushes.Red, 0, 0);
  12. g.DrawEllipse(Pens.Blue, 100, 100, 100, 100);
  13. // 图片保存到输出流
  14. bmp.Save(context.Response.OutputStream, ImageFormat.Jpeg);
  15. }
  16. }

图片中显式访问者浏览器信息

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "image/jpeg";
  4. using (var bmp = new Bitmap(500, 200))
  5. using (var g = Graphics.FromImage(bmp))
  6. using (var font = new Font(FontFamily.GenericSerif, 20))
  7. {
  8. var request = context.Request;
  9. g.Clear(Color.White);
  10. g.DrawString("IP:" + request.UserHostAddress, font, Brushes.DeepSkyBlue, 0, 0);
  11. g.DrawString("浏览器:" + request.Browser.Browser + request.Browser.Version, font, Brushes.DeepSkyBlue, 0, 40);
  12. g.DrawString("操作系统:" + request.Browser.Platform, font, Brushes.DeepSkyBlue, 0, 80);
  13. bmp.Save(context.Response.OutputStream, ImageFormat.Jpeg);
  14. }
  15. }

image.png

动态生成恶搞图片

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "image/jpeg";
  4. var name = context.Request["Name"];
  5. var imgPath = context.Server.MapPath("~/PaoNiuZheng.jpg");
  6. using (Image bmp = Image.FromFile(imgPath))
  7. using (var g = Graphics.FromImage(bmp))
  8. using (var font1 = new Font(FontFamily.GenericSerif, 12))
  9. using (var font2 = new Font(FontFamily.GenericSerif, 5))
  10. {
  11. {
  12. g.DrawString(name, font1, Brushes.Black, 125, 220);
  13. g.DrawString(name, font2, Brushes.Black, 310, 50);
  14. bmp.Save(context.Response.OutputStream, ImageFormat.Jpeg);
  15. }
  16. }
  17. }

image.png

生成验证码

参考 SOF,写了个生成真随机验证码,后续可以添加些麻子点点。
真随机码性能有点低,如果追求性能可以参考这个答案

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "image/jpeg";
  4. using (var bmp = new Bitmap(200, 100))
  5. using (var g = Graphics.FromImage(bmp))
  6. using (var font = new Font(FontFamily.GenericSerif, 10))
  7. {
  8. var random = TrueRandomString(4);
  9. g.Clear(Color.White);
  10. g.DrawString(random,font,Brushes.CadetBlue,0,0);
  11. bmp.Save(context.Response.OutputStream,ImageFormat.Jpeg);
  12. }
  13. }
  14. static string TrueRandomString(int length)
  15. {
  16. //const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
  17. const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  18. var res = new StringBuilder();
  19. using (var rng = new RNGCryptoServiceProvider())
  20. {
  21. byte[] uintBuffer = new byte[sizeof(uint)];
  22. while (length-- > 0)
  23. {
  24. rng.GetBytes(uintBuffer);
  25. // 利用随机的字节数组生成一个 uint 数字
  26. var num = BitConverter.ToUInt32(uintBuffer, 0);
  27. // num 对有效字符长度取模,得到一个有效字符
  28. res.Append(validChars[(int)(num % (uint)validChars.Length)]);
  29. }
  30. }
  31. return res.ToString();
  32. }

image.png

如果对随机数有兴趣,请参阅我的另一篇文章 Random 细节

文件下载

增加报文头告诉浏览器返回的内容是“附件形式”,要给用户保存。

  1. context.Response.AddHeader("Content-Disposition",
  2. "attachment;filename=" + context.Server.UrlEncode("用户数据.txt"));

对于“.ashx”右键另存为其实还是浏览器向服务器发 Http 请求,然后把服务器运行后返回的 Http 报文体保存到文件中。不会把 ashx 的源代码下载下来。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. context.Response.AddHeader("Content-Disposition",
  5. "attachment;filename=" + context.Server.UrlEncode("用户数据.txt"));
  6. using (var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Users"))
  7. {
  8. foreach (DataRow row in dt.Rows)
  9. {
  10. context.Response.Write("Name:" + row["UserName"] + " Age:" + row["Age"] + "\n");
  11. }
  12. }
  13. }

服务器返回报文头里面多了 Content-Disposition:
image.png

导出数据生成 Excel 供下载

NPOI 太丑了,下面使用 Aspose.Cells。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. context.Response.AddHeader("Content-Disposition",
  5. "attachment;filename=" + context.Server.UrlEncode("用户数据.xlsx"));
  6. using (var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Users"))
  7. {
  8. var workbook = new Workbook();
  9. var sheet = workbook.Worksheets[0];
  10. // Header
  11. for (var i = 0; i < dt.Columns.Count; i++)
  12. {
  13. sheet.Cells[0, i].PutValue(dt.Columns[i].ColumnName);
  14. }
  15. // Content
  16. for (var i = 0; i < dt.Rows.Count; i++)
  17. {
  18. for (var j = 0; j < dt.Columns.Count; j++)
  19. {
  20. sheet.Cells[i + 1, j].PutValue(dt.Rows[i][j].ToString());
  21. }
  22. }
  23. workbook.Save(context.Response.OutputStream,SaveFormat.Xlsx);
  24. }
  25. }

image.png

提取码下载图片

输入正确提取码下载图片,否则提示“提取码错误”。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. var fetchCode = context.Request["fetch"];
  5. if (fetchCode == null || fetchCode != "hct")
  6. {
  7. context.Response.Write("提取码错误");
  8. return;
  9. }
  10. context.Response.ContentType = "image/jpeg";
  11. context.Response.AddHeader("Content-Disposition",
  12. "attachment;filename=" + context.Server.UrlEncode("hct.jpeg"));
  13. var filePath = context.Server.MapPath("~/hct.jpg");
  14. using (Stream inStream = File.OpenRead(filePath))
  15. {
  16. inStream.CopyTo(context.Response.OutputStream);
  17. }
  18. }

文件上传

进行文件上传,需要采用 method=”post”,并且设定 enctype=”multipart/form-data”。
文件不可能变成字符串在地址栏中提交(GET)。

报文里不知道具体类型的文件的 ContentType 都是 application/octet-stream。

上传图片示例:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. // 根据 input 的 name 属性获取上传的文件
  5. HttpPostedFile file1 = context.Request.Files["file1"];
  6. file1.SaveAs(context.Server.MapPath("~/upload/" + file1.FileName));
  7. context.Response.Write(file1.FileName);
  8. }

设置过 multipart 后,上传文件的报文格式变了。
不难发现 HttpPostedFile.FileName 也是浏览器提交给服务器的。
image.png

对上传文件进行限制

将用户上传的图片保存到网站的 upload 目录,检查文件大小不能大于1MB(避免撑爆服务器),不能是 jpg、gif、png 之外的图片。

Q:可以用 Request 中的 ContentType 来判断文件类型吗?
A:不能,牢记 Request 是可以伪造的。

下面使用了 FileName 来进行类型判断,但其实这个也能伪造。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. HttpPostedFile file1 = context.Request.Files["file1"];
  5. if (file1.ContentLength > 1024 * 1024)
  6. {
  7. context.Response.Write("图片不能超过 1 MB");
  8. return;
  9. }
  10. var fileExt = Path.GetExtension(file1.FileName);
  11. if (fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".png")
  12. {
  13. context.Response.Write("文件类型不允许");
  14. return;
  15. }
  16. file1.SaveAs(context.Server.MapPath("~/upload/" + file1.FileName));
  17. context.Response.Write("上传成功");
  18. }

图片水印

通过 file.InputStream 获得上传图片的文件流,并在图片上打印水印。

文件不落地原则:用户上传的文件尽量不保存到本地,能用流操作就用流。可以避免服务器撑爆、非法文件造成安全性问题。

像我之前在某个项目里面接收 Excel 文件后,存储在服务器才开始分析的做法就是不提倡的。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. HttpPostedFile file1 = context.Request.Files["file1"];
  5. if (file1.ContentLength > 1024 * 1024)
  6. {
  7. context.Response.Write("图片不能超过 1 MB");
  8. return;
  9. }
  10. var fileExt = Path.GetExtension(file1.FileName);
  11. if (fileExt != ".jpg" && fileExt != ".gif" && fileExt != ".png")
  12. {
  13. context.Response.Write("文件类型不允许");
  14. return;
  15. }
  16. // 文件不落地原则:用户上传的文件尽量不保存到本地
  17. // 可以避免服务器撑爆、非法文件造成安全性问题
  18. using (Image img = Image.FromStream(file1.InputStream))
  19. {
  20. using (Graphics g = Graphics.FromImage(img))
  21. using (var font = new Font(FontFamily.GenericSerif, 20))
  22. {
  23. g.DrawString("Wonder",font,Brushes.SteelBlue,0,0);
  24. }
  25. img.Save(context.Response.OutputStream,img.RawFormat);
  26. }
  27. }

image.png

WebExcel

通过 Web 展示上传的 Excel。

下面代码只做演示,实际情况需检查上传文件类型并考虑 Excel 版本兼容问题。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var writeSb = new StringBuilder();
  5. writeSb.Append("<html><head><title>ExcelWeb</title>");
  6. writeSb.Append("<style>table{border-collapse: collapse;}table, th, td {border: 1px solid #D4D4D4;}</style></head>");
  7. writeSb.Append("<body>");
  8. var file1 = context.Request.Files["file1"];
  9. var workbook = new Workbook(file1.InputStream);
  10. var sheet = workbook.Worksheets[0];
  11. writeSb.Append("<h1>" + sheet.Name + "</h1>");
  12. writeSb.Append("<table>");
  13. foreach (Row row in sheet.Cells.Rows)
  14. {
  15. writeSb.Append("<tr>");
  16. for (var i = sheet.Cells.MinColumn; i < sheet.Cells.MaxColumn; i++)
  17. {
  18. writeSb.Append("<td>" + row[i].StringValue + "</td>");
  19. }
  20. writeSb.Append("</tr>");
  21. }
  22. writeSb.Append("</table></body></html>");
  23. context.Response.Write(writeSb.ToString());
  24. }

image.png