本章主干知识点:
- 知道模板文件就是简化 HTML 的拼接而已
- 能够使用 HttpHandler + 模板 实现增删改查
- 知道 C# 代码是运行在服务器上的
- 知道所有浏览器发送给服务器的请求都是可以造假的,是不可信的
模版文件
开发一个 ViewPerson.ashx 页面传递 id=1 时,显示 T_Persons 表里面 id=1 的人员信息。
总是拼写 HTML 太麻烦,不仅要注意双引号的问题,而且如果要对页面美化或者写 JavaScript 都要改 C# 代码。
模板文件即把页面逻辑放到单独的文件中,变化的地方定义成占位符,ashx 输出时再替换占位符填充数据。
模版文件后缀是什么都无所谓,用 .html 只是为了方便 IDE 自动提示。
Person 类对应的 T_Persons 表结构:
没有模板文件
没有模板文件,用纯 ashx。手动拼写 HTML,难以进行页面美化。
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";context.Response.Write("<html><head><title></title></head><body>");var id = Convert.ToInt32(context.Request["id"]);var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id", new SqlParameter("@Id", id));if (dt.Rows.Count <= 0){context.Response.Write("Id not found!");context.Response.Write("</body></html>");return;}var row = dt.Rows[0];var name = Convert.ToString(row["Name"]);var age = Convert.ToInt32(row["Age"]);var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";context.Response.Write("<table><tr><td>姓名</td><td>年龄</td><td>性别</td></tr>");context.Response.Write("<tr><td>" + name + "</td><td>" + age + "</td><td>" + gender + "</td></tr>");context.Response.Write("</table></body></html>");}
效果:
使用模板后
使用模板后,不用再手动拼写 HTML,只需最后进行数据填充即可。
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var id = Convert.ToInt32(context.Request["id"]);var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id", new SqlParameter("@Id", id));if (dt.Rows.Count <= 0){var errorFileName = context.Server.MapPath("~/PersonView2Error.html");var errorHtml = File.ReadAllText(errorFileName);errorHtml = errorHtml.Replace("@msg", "Id 不存在!");context.Response.Write(errorHtml);return;}var row = dt.Rows[0];var name = Convert.ToString(row["Name"]);var age = Convert.ToInt32(row["Age"]);var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";var personViewHtmlFileName = context.Server.MapPath("~/PersonView2.html");var personViewHtml = File.ReadAllText(personViewHtmlFileName);personViewHtml = personViewHtml.Replace("@name", name).Replace("@age", age.ToString()).Replace("@gender", gender);context.Response.Write(personViewHtml);}
模板文件:
可在文件内随意添加 CSS、JS 代码优化显式效果。
<!DOCTYPE html><html><head><meta charset="utf-8" /><title>@name</title></head><body><table><tr style="color:springgreen"><td>姓名</td><td>年龄</td><td>性别</td></tr><tr><td>@name</td><td>@age</td><td>@gender</td></tr></table></body></html>

使用封装优化模板文件
DRY 原则
将输出错误信息封装到 OutputError 方法里面。
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";if (string.IsNullOrEmpty(context.Request["id"])){OutputError(context, "Id 不能为空");return;}var id = Convert.ToInt32(context.Request["id"]);var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id", new SqlParameter("@Id", id));if (dt.Rows.Count <= 0){OutputError(context, "Id 不存在!");return;}var row = dt.Rows[0];var name = Convert.ToString(row["Name"]);var age = Convert.ToInt32(row["Age"]);var gender = Convert.ToBoolean(row["Gender"]) ? "男" : "女";var personViewHtml = CommonHelper.ReadHtml("~/PersonView2.html");personViewHtml = personViewHtml.Replace("@name", name).Replace("@age", age.ToString()).Replace("@gender", gender);context.Response.Write(personViewHtml);}private void OutputError(HttpContext context, string msg){var errorHtml = CommonHelper.ReadHtml("~/PersonView2Error.html");errorHtml = errorHtml.Replace("@msg", msg);context.Response.Write(errorHtml);}
将读取 HTML 封装到 CommonHelper 里面。
public class CommonHelper{/// <summary>/// Read the file undr the virtual path, and return content./// </summary>/// <param name="virtualPath"></param>/// <returns></returns>public static string ReadHtml(string virtualPath){var fullPath = HttpContext.Current.Server.MapPath(virtualPath);return File.ReadAllText(fullPath);}}
查人员列表
因为没有强大的模板引擎(aspx、razor、NVelocity 等),所以 Table 部分还是采用字符串拼接。
ashx:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");var sb = new StringBuilder();foreach (DataRow row in dt.Rows){sb.Append("<tr>");sb.Append("<td>").Append(row["name"]).Append("</td><td>").Append(row["age"]).Append("</td>");sb.Append("<tr>");}var html = CommonHelper.ReadHtml("~/PersonList.html");html = html.Replace("{persons}", sb.ToString());context.Response.Write(html);}
HTML:
<!DOCTYPE html><html><head><meta charset="utf-8" /><title>人员列表</title></head><body><table><thead><tr><th>姓名</th><th>年龄</th></tr></thead><tbody>{persons}</tbody></table></body></html>
效果:
删除人员
做一个删除的 HttpHandler,把要删除行的 id 传递给 ashx。
PersonDelete.ashx:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var id = Convert.ToInt32(context.Request["id"]);SqlHelper.ExecuteNonQuery("DELETE FROM T_Persons WHERE Id=@Id",new SqlParameter("@Id", id));// 删除后,重定向回查看页面context.Response.Redirect("PersonList.ashx");}
增加删除功能后的 PersonList.ashx:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");var sb = new StringBuilder();foreach (DataRow row in dt.Rows){sb.Append("<tr>").Append("<td>").Append(row["name"]).Append("</td>").Append("<td>").Append(row["age"]).Append("</td>").Append("<td> <a href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>").Append("<tr>");}var html = CommonHelper.ReadHtml("~/PersonList.html");html = html.Replace("{persons}", sb.ToString());context.Response.Write(html);}
效果:
确认删除对话框
通过 JS 代码实现“确认删除”对话框。
注:删除代码是运行在浏览器上的。
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons");var sb = new StringBuilder();foreach (DataRow row in dt.Rows){sb.Append("<tr>").Append("<td>").Append(row["name"]).Append("</td>").Append("<td>").Append(row["age"]).Append("</td>").Append("<td> <a onclick='return confirm(\"你真的要删除吗?\")' href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>").Append("<tr>");}var html = CommonHelper.ReadHtml("~/PersonList.html");html = html.Replace("{persons}", sb.ToString());context.Response.Write(html);}

新增和编辑人员
新增和编辑的界面几乎一样,为了复用,把它们的 PersonEditAddNew.ashx 和模板文件放到一起。
把 PersonView2Error.html 进一步提炼为 Error.html,CommonHelper 里也增加 OutputError 方法。
public static void OutputError(string msg){var errorHtml = ReadHtml("~Error.html").Replace("{msg}", msg);HttpContext.Current.Response.Write(errorHtml);}
PersonSave
新增或编辑完毕后,点击“保存”按钮将数据提交给 PersonSave.ashx 进行处理。
- 通过一个隐藏 input 来区分编辑、新增
- 通过一个隐藏 input 来传递编辑行的 Id
最终的模板文件 PersonEditAddNew.html:
<!DOCTYPE html><html><head><meta charset="utf-8" /><title>{actionName}人员</title></head><body><form action="PersonSave.ashx" method="post"><input type="hidden" name="action" value="{action}" /><input type="hidden" name="id" value="{id}" />姓名:<input type="text" name="name" value="{name}" />年龄:<input type="text" name="age" value="{age}" />性别(选上即男):<input type="checkbox" name="gender" {gender} /><input type="submit" value="保存" /></form></body></html>

PersonEditAddNew.ashx:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var html = CommonHelper.ReadHtml("~/PersonEditAddNew.html");var action = context.Request["action"];if (action == "addnew"){html = html.Replace("{actionName}", "新增").Replace("{action}", "addnew").Replace("{name}", "").Replace("{age}", "18").Replace("{gender}", "checked"); ;context.Response.Write(html);}else if (action == "edit"){var id = Convert.ToInt32(context.Request["id"]);var dt = SqlHelper.ExecuteQuery("SELECT * FROM T_Persons WHERE Id=@Id",new SqlParameter("@Id", id));if (dt.Rows.Count <= 0){CommonHelper.OutputError("没找到 id=" + id + "的人员");return;}if (dt.Rows.Count > 1){CommonHelper.OutputError("找到多条 id=" + id + "的人员");return;}var row = dt.Rows[0];var name = row["name"].ToString();var age = row["age"].ToString();var gender = Convert.ToBoolean(row["gender"]);html = html.Replace("{actionName}", "编辑").Replace("{action}", "edit").Replace("{id}", id.ToString()).Replace("{name}", name).Replace("{age}", age).Replace("{gender}", gender ? "checked" : "");context.Response.Write(html);}else{CommonHelper.OutputError("Action 错误!");}}
PersonSave.ashx:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";var action = context.Request["action"];if (action != "edit" && action != "addnew"){CommonHelper.OutputError("action 错误!");return;}var strName = context.Request["name"];var strAge = context.Request["age"];var strGender = context.Request["gender"];if (string.IsNullOrEmpty(strName)){CommonHelper.OutputError("姓名不能为空!");return;}if (string.IsNullOrEmpty(strAge)){CommonHelper.OutputError("年龄不能为空!");return;}int age;if (!int.TryParse(strAge, out age)){CommonHelper.OutputError("年龄不是合法整数值!");return;}if (action == "edit"){var id = Convert.ToInt32(context.Request["id"]);SqlHelper.ExecuteNonQuery("UPDATE T_Persons SET Name=@Name,Age=@Age,Gender=@Gender WHERE Id=@Id",new SqlParameter("@Name", strName),new SqlParameter("@Age", age),new SqlParameter("Gender", strGender != null),new SqlParameter("Id", id));}else if (action == "addnew"){SqlHelper.ExecuteNonQuery("INSERT INTO T_Persons(Name,Age,Gender) VALUES(@Name,@Age,@Gender)",new SqlParameter("@Name", strName),new SqlParameter("@Age", age),new SqlParameter("Gender", strGender != null));}context.Response.Redirect("PersonList.ashx");}
公司的 CRUD
该示例主要为了复习上面的内容和讲解 HTML Select 控件的用法。
Select 的每个 option 都有 value,选定的 option 设定为 selected。
T_Companys 表结构
注:ManagerId 是指向 T_Persons 表的外键
课后练习
构建一个学生管理系统。
数据库:
学生:姓名、性别、生日、身高、所属班级(select 选择)、是否特长生
老师:姓名、电话、邮箱、生日
班级:班级名称、教室号、班主任老师(select 选择)
学生、老师、班级一共三套增删改查。
Web 注意点
C# 在服务器,JS 在客户端
弹出消息示例
在服务器端“弹出消息窗口”:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";context.Response.Write("<script type='text/javascript'>alert('删除成功')</script>");}
这就是在服务器端生成了一串 HTML 字符串,真正的弹出对话框还是在浏览器解析后执行的。
弹出对话框后跳转的错误写法:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";context.Response.Write("<script type='text/javascript'>alert('删除成功')</script>");// 有人希望借此实现,点击确认框后会到 Safe1.html// 真实情况是,服务器不会先输出 Alert 等浏览器解析后再向下执行// 而是执行完所有 C# 代码后,浏览器再对其进行解析// 在这里表现为直接跳转页面,不弹出对话框context.Response.Redirect("Safe1.html");}
而在 C# 中 MessageBox.Show 则是弹出在服务器上的。
一种正确写法:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/html";context.Response.Write("<script type='text/javascript'>alert('删除成功');location.href='Safe1.html';</script>");}
创建木马文件
伟大的ASP.NET,可以在访问者磁盘中创建木马文件!!
File.WriteAllBytes("d:/muma.exe",new byte[]{33,55,88,99,232,22,11,33});
因为 C# 代码是运行在服务器中的,所以 exe 只会生成到服务器的磁盘中,浏览器得到的只有返回的 HTML 内容。
不要相信浏览器
客户端验证不能代替服务端验证
设置取款金额不能高于 1000 元。
若只使用客户端验证,用户可以直接向服务器发 Http 请求(比如直接在地址栏中构造querystring)绕过客户端浏览器检查来干坏事。
客户端校验只是为了提升用户体验,服务器端校验才是把关,防止恶意请求。一个都不能少。
服务器端验证:
public void ProcessRequest(HttpContext context){context.Response.ContentType = "text/plain";var amount = Convert.ToInt32(context.Request["amount"]);if (amount > 1000){context.Response.Write("金额不能大于 1000");return;}context.Response.Write("取款成功");}
客户端验证:
<!DOCTYPE html><html><head><meta charset="utf-8" /><title></title><script type="text/javascript">function checkForm() {var amount = parseInt(document.getElementById("amount").value);if (amount > 1000) {alert("金额不能大于 1000");return false;}return true;}</script></head><body><form action="ATM1.ashx" method="post" onsubmit="return checkForm();">取款金额:<input type="text" name="amount" id="amount" /><input type="submit" value="取款" /></form></body></html>
客户端提交的数据都是能篡改的
客户端藏起来、不显示也不一定安全。
以 PersonList.ashx 为例,隐藏 id 为偶数的删除按钮。
if (id % 2 == 0){sb.Append("<tr>").Append("<td>").Append(row["name"]).Append("</td>").Append("<td>").Append(row["age"]).Append("</td>").Append("<td> <a href='PersonEditAddNew.ashx?action=edit&id=").Append(row["id"]).Append("'>编辑</a></td>").Append("<tr>");}else{sb.Append("<tr>").Append("<td>").Append(row["name"]).Append("</td>").Append("<td>").Append(row["age"]).Append("</td>").Append("<td> <a onclick='return confirm(\"确认删除?\")' href='PersonDelete.ashx?id=").Append(row["id"]).Append("'>删除</a></td>").Append("<td> <a href='PersonEditAddNew.ashx?action=edit&id=").Append(row["id"]).Append("'>编辑</a></td>").Append("<tr>");}
用户不是傻子,它一看 <a href='PersonEditAddNew.ashx?action=edit&id=1' /> 就很可能会自己拼接 url 进行删除。
同样还是应该在服务器端进行 id 检查。
HTTP 报文的 UserAgent、Referer、Cookie 等都是可以造假的,不要相信这些可能会造假的数据。
注:IP地址不可以造假,但是可以通过代理服务器来转发,服务器拿到的就是代理服务器的 IP。
Q:投票网站如何避免刷票 ?
A:关键在于如何确定用户有没有投过票。
- 不能通过 IP 进行确定,有的宿舍、小区拉一条光纤,他们的外网 IP 都是一样的
- 不能通过 MAC 地址,互联网中 MAC 地址不会通过 HTTP 协议传输,无法获取
- 局域网中通过一些特定技术可以获得
- 最简单的方法就是通过手机号注册验证

