本章主干知识点

  • 知道模板文件就是简化 HTML 的拼接而已
  • 能够使用 HttpHandler + 模板 实现增删改查
  • 知道 C# 代码是运行在服务器上的
  • 知道所有浏览器发送给服务器的请求都是可以造假的,是不可信的

模版文件

开发一个 ViewPerson.ashx 页面传递 id=1 时,显示 T_Persons 表里面 id=1 的人员信息。

总是拼写 HTML 太麻烦,不仅要注意双引号的问题,而且如果要对页面美化或者写 JavaScript 都要改 C# 代码。

模板文件即把页面逻辑放到单独的文件中,变化的地方定义成占位符,ashx 输出时再替换占位符填充数据。

模版文件后缀是什么都无所谓,用 .html 只是为了方便 IDE 自动提示。

Person 类对应的 T_Persons 表结构:
image.png

没有模板文件

没有模板文件,用纯 ashx。手动拼写 HTML,难以进行页面美化。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. context.Response.Write("<html><head><title></title></head><body>");
  5. var id = Convert.ToInt32(context.Request["id"]);
  6. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id", new SqlParameter("@Id", id));
  7. if (dt.Rows.Count <= 0)
  8. {
  9. context.Response.Write("Id not found!");
  10. context.Response.Write("</body></html>");
  11. return;
  12. }
  13. var row = dt.Rows[0];
  14. var name = Convert.ToString(row["Name"]);
  15. var age = Convert.ToInt32(row["Age"]);
  16. var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";
  17. context.Response.Write("<table><tr><td>姓名</td><td>年龄</td><td>性别</td></tr>");
  18. context.Response.Write("<tr><td>" + name + "</td><td>" + age + "</td><td>" + gender + "</td></tr>");
  19. context.Response.Write("</table></body></html>");
  20. }

效果:
image.png

使用模板后

使用模板后,不用再手动拼写 HTML,只需最后进行数据填充即可。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var id = Convert.ToInt32(context.Request["id"]);
  5. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id"
  6. , new SqlParameter("@Id", id));
  7. if (dt.Rows.Count <= 0)
  8. {
  9. var errorFileName = context.Server.MapPath("~/PersonView2Error.html");
  10. var errorHtml = File.ReadAllText(errorFileName);
  11. errorHtml = errorHtml.Replace("@msg", "Id 不存在!");
  12. context.Response.Write(errorHtml);
  13. return;
  14. }
  15. var row = dt.Rows[0];
  16. var name = Convert.ToString(row["Name"]);
  17. var age = Convert.ToInt32(row["Age"]);
  18. var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";
  19. var personViewHtmlFileName = context.Server.MapPath("~/PersonView2.html");
  20. var personViewHtml = File.ReadAllText(personViewHtmlFileName);
  21. personViewHtml = personViewHtml.Replace("@name", name)
  22. .Replace("@age", age.ToString())
  23. .Replace("@gender", gender);
  24. context.Response.Write(personViewHtml);
  25. }

模板文件:
可在文件内随意添加 CSS、JS 代码优化显式效果。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>@name</title>
  6. </head>
  7. <body>
  8. <table>
  9. <tr style="color:springgreen"><td>姓名</td><td>年龄</td><td>性别</td></tr>
  10. <tr><td>@name</td><td>@age</td><td>@gender</td></tr>
  11. </table>
  12. </body>
  13. </html>

image.png

使用封装优化模板文件

DRY 原则

将输出错误信息封装到 OutputError 方法里面。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. if (string.IsNullOrEmpty(context.Request["id"]))
  5. {
  6. OutputError(context, "Id 不能为空");
  7. return;
  8. }
  9. var id = Convert.ToInt32(context.Request["id"]);
  10. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id"
  11. , new SqlParameter("@Id", id));
  12. if (dt.Rows.Count <= 0)
  13. {
  14. OutputError(context, "Id 不存在!");
  15. return;
  16. }
  17. var row = dt.Rows[0];
  18. var name = Convert.ToString(row["Name"]);
  19. var age = Convert.ToInt32(row["Age"]);
  20. var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";
  21. var personViewHtml = CommonHelper.ReadHtml("~/PersonView2.html");
  22. personViewHtml = personViewHtml.Replace("@name", name)
  23. .Replace("@age", age.ToString())
  24. .Replace("@gender", gender);
  25. context.Response.Write(personViewHtml);
  26. }
  27. private void OutputError(HttpContext context, string msg)
  28. {
  29. var errorHtml = CommonHelper.ReadHtml("~/PersonView2Error.html");
  30. errorHtml = errorHtml.Replace("@msg", msg);
  31. context.Response.Write(errorHtml);
  32. }

将读取 HTML 封装到 CommonHelper 里面。

  1. public class CommonHelper
  2. {
  3. /// <summary>
  4. /// Read the file undr the virtual path, and return content.
  5. /// </summary>
  6. /// <param name="virtualPath"></param>
  7. /// <returns></returns>
  8. public static string ReadHtml(string virtualPath)
  9. {
  10. var fullPath = HttpContext.Current.Server.MapPath(virtualPath);
  11. return File.ReadAllText(fullPath);
  12. }
  13. }

查人员列表

因为没有强大的模板引擎(aspx、razor、NVelocity 等),所以 Table 部分还是采用字符串拼接。

ashx:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");
  5. var sb = new StringBuilder();
  6. foreach (DataRow row in dt.Rows)
  7. {
  8. sb.Append("<tr>");
  9. sb.Append("<td>").Append(row["name"]).Append("</td><td>")
  10. .Append(row["age"]).Append("</td>");
  11. sb.Append("<tr>");
  12. }
  13. var html = CommonHelper.ReadHtml("~/PersonList.html");
  14. html = html.Replace("{persons}", sb.ToString());
  15. context.Response.Write(html);
  16. }

HTML:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>人员列表</title>
  6. </head>
  7. <body>
  8. <table>
  9. <thead>
  10. <tr><th>姓名</th><th>年龄</th></tr>
  11. </thead>
  12. <tbody>
  13. {persons}
  14. </tbody>
  15. </table>
  16. </body>
  17. </html>

效果:
image.png

删除人员

做一个删除的 HttpHandler,把要删除行的 id 传递给 ashx。

PersonDelete.ashx:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var id = Convert.ToInt32(context.Request["id"]);
  5. SqlHelper.ExecuteNonQuery("DELETE FROM T_Persons WHERE Id=@Id",
  6. new SqlParameter("@Id", id));
  7. // 删除后,重定向回查看页面
  8. context.Response.Redirect("PersonList.ashx");
  9. }

增加删除功能后的 PersonList.ashx:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");
  5. var sb = new StringBuilder();
  6. foreach (DataRow row in dt.Rows)
  7. {
  8. sb.Append("<tr>")
  9. .Append("<td>").Append(row["name"]).Append("</td>")
  10. .Append("<td>").Append(row["age"]).Append("</td>")
  11. .Append("<td> <a href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>")
  12. .Append("<tr>");
  13. }
  14. var html = CommonHelper.ReadHtml("~/PersonList.html");
  15. html = html.Replace("{persons}", sb.ToString());
  16. context.Response.Write(html);
  17. }

效果:
image.png

image.png

确认删除对话框

通过 JS 代码实现“确认删除”对话框。
注:删除代码是运行在浏览器上的。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");
  5. var sb = new StringBuilder();
  6. foreach (DataRow row in dt.Rows)
  7. {
  8. sb.Append("<tr>")
  9. .Append("<td>").Append(row["name"]).Append("</td>")
  10. .Append("<td>").Append(row["age"]).Append("</td>")
  11. .Append("<td> <a onclick='return confirm(\"你真的要删除吗?\")' href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>")
  12. .Append("<tr>");
  13. }
  14. var html = CommonHelper.ReadHtml("~/PersonList.html");
  15. html = html.Replace("{persons}", sb.ToString());
  16. context.Response.Write(html);
  17. }

image.png

新增和编辑人员

新增和编辑的界面几乎一样,为了复用,把它们的 PersonEditAddNew.ashx 和模板文件放到一起。

把 PersonView2Error.html 进一步提炼为 Error.html,CommonHelper 里也增加 OutputError 方法。

  1. public static void OutputError(string msg)
  2. {
  3. var errorHtml = ReadHtml("~Error.html").Replace("{msg}", msg);
  4. HttpContext.Current.Response.Write(errorHtml);
  5. }

PersonSave

新增或编辑完毕后,点击“保存”按钮将数据提交给 PersonSave.ashx 进行处理。

  • 通过一个隐藏 input 来区分编辑、新增
  • 通过一个隐藏 input 来传递编辑行的 Id

最终的模板文件 PersonEditAddNew.html:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>{actionName}人员</title>
  6. </head>
  7. <body>
  8. <form action="PersonSave.ashx" method="post">
  9. <input type="hidden" name="action" value="{action}" />
  10. <input type="hidden" name="id" value="{id}" />
  11. 姓名:<input type="text" name="name" value="{name}" />年龄:<input type="text" name="age" value="{age}" />
  12. 性别(选上即男):<input type="checkbox" name="gender" {gender} />
  13. <input type="submit" value="保存" />
  14. </form>
  15. </body>
  16. </html>

image.png

PersonEditAddNew.ashx:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var html = CommonHelper.ReadHtml("~/PersonEditAddNew.html");
  5. var action = context.Request["action"];
  6. if (action == "addnew")
  7. {
  8. html = html.Replace("{actionName}", "新增").Replace("{action}", "addnew")
  9. .Replace("{name}", "").Replace("{age}", "18").Replace("{gender}", "checked"); ;
  10. context.Response.Write(html);
  11. }
  12. else if (action == "edit")
  13. {
  14. var id = Convert.ToInt32(context.Request["id"]);
  15. var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id",
  16. new SqlParameter("@Id", id));
  17. if (dt.Rows.Count <= 0)
  18. {
  19. CommonHelper.OutputError("没找到 id=" + id + "的人员");
  20. return;
  21. }
  22. if (dt.Rows.Count > 1)
  23. {
  24. CommonHelper.OutputError("找到多条 id=" + id + "的人员");
  25. return;
  26. }
  27. var row = dt.Rows[0];
  28. var name = row["name"].ToString();
  29. var age = row["age"].ToString();
  30. var gender = Convert.ToBoolean(row["gender"]);
  31. html = html.Replace("{actionName}", "编辑").Replace("{action}", "edit").Replace("{id}", id.ToString())
  32. .Replace("{name}", name).Replace("{age}", age).Replace("{gender}", gender ? "checked" : "");
  33. context.Response.Write(html);
  34. }
  35. else
  36. {
  37. CommonHelper.OutputError("Action 错误!");
  38. }
  39. }

PersonSave.ashx:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. var action = context.Request["action"];
  5. if (action != "edit" && action != "addnew")
  6. {
  7. CommonHelper.OutputError("action 错误!");
  8. return;
  9. }
  10. var strName = context.Request["name"];
  11. var strAge = context.Request["age"];
  12. var strGender = context.Request["gender"];
  13. if (string.IsNullOrEmpty(strName))
  14. {
  15. CommonHelper.OutputError("姓名不能为空!");
  16. return;
  17. }
  18. if (string.IsNullOrEmpty(strAge))
  19. {
  20. CommonHelper.OutputError("年龄不能为空!");
  21. return;
  22. }
  23. int age;
  24. if (!int.TryParse(strAge, out age))
  25. {
  26. CommonHelper.OutputError("年龄不是合法整数值!");
  27. return;
  28. }
  29. if (action == "edit")
  30. {
  31. var id = Convert.ToInt32(context.Request["id"]);
  32. SqlHelper.ExecuteNonQuery("UPDATE T_Persons SET Name=@Name,Age=@Age,Gender=@Gender WHERE Id=@Id",
  33. new SqlParameter("@Name", strName),
  34. new SqlParameter("@Age", age),
  35. new SqlParameter("Gender", strGender != null),
  36. new SqlParameter("Id", id));
  37. }
  38. else if (action == "addnew")
  39. {
  40. SqlHelper.ExecuteNonQuery("INSERT INTO T_Persons(Name,Age,Gender) VALUES(@Name,@Age,@Gender)",
  41. new SqlParameter("@Name", strName),
  42. new SqlParameter("@Age", age),
  43. new SqlParameter("Gender", strGender != null));
  44. }
  45. context.Response.Redirect("PersonList.ashx");
  46. }

公司的 CRUD

该示例主要为了复习上面的内容和讲解 HTML Select 控件的用法。

Select 的每个 option 都有 value,选定的 option 设定为 selected。

T_Companys 表结构
注:ManagerId 是指向 T_Persons 表的外键
image.png

公司 CRUD.zip

课后练习

构建一个学生管理系统。

数据库:
学生:姓名、性别、生日、身高、所属班级(select 选择)、是否特长生
老师:姓名、电话、邮箱、生日
班级:班级名称、教室号、班主任老师(select 选择)

学生、老师、班级一共三套增删改查。

Web 注意点

C# 在服务器,JS 在客户端

弹出消息示例

在服务器端“弹出消息窗口”:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. context.Response.Write("<script type='text/javascript'>alert('删除成功')</script>");
  5. }

这就是在服务器端生成了一串 HTML 字符串,真正的弹出对话框还是在浏览器解析后执行的。

弹出对话框后跳转的错误写法:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. context.Response.Write("<script type='text/javascript'>alert('删除成功')</script>");
  5. // 有人希望借此实现,点击确认框后会到 Safe1.html
  6. // 真实情况是,服务器不会先输出 Alert 等浏览器解析后再向下执行
  7. // 而是执行完所有 C# 代码后,浏览器再对其进行解析
  8. // 在这里表现为直接跳转页面,不弹出对话框
  9. context.Response.Redirect("Safe1.html");
  10. }

而在 C# 中 MessageBox.Show 则是弹出在服务器上的。

一种正确写法:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/html";
  4. context.Response.Write("<script type='text/javascript'>alert('删除成功');location.href='Safe1.html';</script>");
  5. }

创建木马文件

伟大的ASP.NET,可以在访问者磁盘中创建木马文件!!

  1. File.WriteAllBytes("d:/muma.exe",new byte[]{33,55,88,99,232,22,11,33});

因为 C# 代码是运行在服务器中的,所以 exe 只会生成到服务器的磁盘中,浏览器得到的只有返回的 HTML 内容。

不要相信浏览器

客户端验证不能代替服务端验证

设置取款金额不能高于 1000 元。

若只使用客户端验证,用户可以直接向服务器发 Http 请求(比如直接在地址栏中构造querystring)绕过客户端浏览器检查来干坏事。

客户端校验只是为了提升用户体验,服务器端校验才是把关,防止恶意请求。一个都不能少。

服务器端验证:

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. context.Response.ContentType = "text/plain";
  4. var amount = Convert.ToInt32(context.Request["amount"]);
  5. if (amount > 1000)
  6. {
  7. context.Response.Write("金额不能大于 1000");
  8. return;
  9. }
  10. context.Response.Write("取款成功");
  11. }

客户端验证:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title></title>
  6. <script type="text/javascript">
  7. function checkForm() {
  8. var amount = parseInt(document.getElementById("amount").value);
  9. if (amount > 1000) {
  10. alert("金额不能大于 1000");
  11. return false;
  12. }
  13. return true;
  14. }
  15. </script>
  16. </head>
  17. <body>
  18. <form action="ATM1.ashx" method="post" onsubmit="return checkForm();">
  19. 取款金额:<input type="text" name="amount" id="amount" />
  20. <input type="submit" value="取款" />
  21. </form>
  22. </body>
  23. </html>

客户端提交的数据都是能篡改的

客户端藏起来、不显示也不一定安全。

以 PersonList.ashx 为例,隐藏 id 为偶数的删除按钮。

  1. if (id % 2 == 0)
  2. {
  3. sb.Append("<tr>")
  4. .Append("<td>").Append(row["name"]).Append("</td>")
  5. .Append("<td>").Append(row["age"]).Append("</td>")
  6. .Append("<td> <a href='PersonEditAddNew.ashx?action=edit&id=").Append(row["id"]).Append("'>编辑</a></td>")
  7. .Append("<tr>");
  8. }
  9. else
  10. {
  11. sb.Append("<tr>")
  12. .Append("<td>").Append(row["name"]).Append("</td>")
  13. .Append("<td>").Append(row["age"]).Append("</td>")
  14. .Append("<td> <a onclick='return confirm(\"确认删除?\")' href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>")
  15. .Append("<td> <a href='PersonEditAddNew.ashx?action=edit&id=").Append(row["id"]).Append("'>编辑</a></td>")
  16. .Append("<tr>");
  17. }

用户不是傻子,它一看 <a href='PersonEditAddNew.ashx?action=edit&id=1' /> 就很可能会自己拼接 url 进行删除。

同样还是应该在服务器端进行 id 检查。

HTTP 报文的 UserAgent、Referer、Cookie 等都是可以造假的,不要相信这些可能会造假的数据。

注:IP地址不可以造假,但是可以通过代理服务器来转发,服务器拿到的就是代理服务器的 IP。

Q:投票网站如何避免刷票 ?
A:关键在于如何确定用户有没有投过票。

  1. 不能通过 IP 进行确定,有的宿舍、小区拉一条光纤,他们的外网 IP 都是一样的
  2. 不能通过 MAC 地址,互联网中 MAC 地址不会通过 HTTP 协议传输,无法获取
    1. 局域网中通过一些特定技术可以获得
  3. 最简单的方法就是通过手机号注册验证