本章主干知识点:
- 知道模板文件就是简化 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 协议传输,无法获取
- 局域网中通过一些特定技术可以获得
- 最简单的方法就是通过手机号注册验证