CMS项目实践学习总结
这个暑假把ITCAST的2011年2月的.NET就业班的视频看了一遍,因为以前把免费的2010版和2011版的视频都看了一遍,所以这次选择了其中的几个部分作为重点学习对象。一个就是呼叫中心项目(前面已经做了总结),一个是.NET高级特性讲解(委托、事件、GC、CLR等等),另一个是图书商城项目(主要是基于WebForm的,拖着快速过了一遍,技术点都已在以前学校的项目中练过),还有这个如鹏网CMS系统(重点在于大访问量的互联网网站开发技术上),最后是ASP.NET MVC开发方式。高级特性部分掌握的不是很熟练,还需要通过《你必须知道的.NET》来巩固一下,重点在于深入.NET Framework。而对于ASP.NET MVC,重点在于了解MVC模型的原理、与WebForm的差别,后续会选择一个基于MVC的项目来巩固实践。这次对CMS系统开发学习做一个小小总结,以便梳理一下这段时间以来的学习(白天忙,晚上学习,很累很值得)。
(1)Membership用户管理框架
用户系统可以自己写,也可以用ASP.Net提供的Membership 。Membership是ASP.Net提供的用户管理架构,和ASP.Net的安全模型结合的最好。可以很好的实现权限验证、权限组等。 Membership只是微软提供的一些BLL,也是微软的人写的。
不用问“到底该用哪个好”之类的问题,条条大路通罗马。如果不喜欢微软的表设计等,可以编写自己的MembershipProvider,不改变使用的API,这就是设计模式的优点。如果想存到其他数据库中只要实现OracleMembershipProvider等即可。
Membership API就是替我们去操作数据库、Cookie,也是ADO.Net等,没有多神奇。
在这个视频学习中,CMS主要使用Membership API实现用户信息扩展:因为Membership中只保存了用户名、Email等简单信息,如果要保存QQ、性别等额外信息需要使用新表。
经过学习后不可否认,Membership给我们开发中创造了很大的便利,其方便的Roles功能,对于我们进行权限管理的时候提供了很好的解决方案。
关于如何使用Membership API,可以参考官方API,也可以参考这篇博文:http://www.cnblogs.com/fnchenlei/archive/2009/12/23/1630819.html
(2)集成邮件系统发送
首先邮件发送的原理图如下:
电子邮件的工作过程遵循客户-服务器模式。每份电子邮件的发送都要涉及到发送方与接收方,发送方式构成客户端,而接收方构成服务器,服务器含有众多用户的电子信箱。发送方通过邮件客户程序,将编辑好的电子邮件向邮局服务器(SMTP服务器)发送。邮局服务器识别接收者的地址,并向管理该地址的邮件服务器(POP3服务器)发送消息。邮件服务器识将消息存放在接收者的电子信箱内,并告知接收者有新邮件到来。接收者通过邮件客户程序连接到服务器后,就会看到服务器的通知,进而打开自己的电子信箱来查收邮件。
在此CMS系统实践学习中,使用了Magic winmail来搭建了一个内网的邮件服务器,它支持WebMail。另外配合Outlook Express客户端来连接服务器接收邮件服务。在配置Magic winmail邮件服务器中,如果在“系统设置”→“系统服务”中看到SMTP服务是停止状态,则表示25端口被占用,用“netstat -ano”发现是inetinfo也就是IIS的SMTP服务占用了端口,因此在IIS中把SMTP服务停掉,再到“系统设置”→“系统服务”中尝试启动SMTP服务。还要确保域名管理有一个域名,用户管理添加用户名、密码等。
当然,最重要的还是如何在ASP.NET中进行邮件任务。下面介绍几种常见的任务代码:
2.1 普通文本邮件发送代码
MailMessage mailMsg = new MailMessage();//两个类,别混了 引入System.Web这个Assembly
mailMsg.From = new MailAddress("admin@rupeng.com", "新广源集团客服中心");//源邮件地址
mailMsg.To.Add(new MailAddress("yzk@rupeng.com", "杨中科"));//目的邮件地址。可以有多个收件人
mailMsg.Subject = "关于.net培训班咨询事宜";//发送邮件的标题
mailMsg.Body = "附件中是资料,请查收!";//发送邮件的内容
SmtpClient client = new SmtpClient("127.0.0.1");
client.Credentials = new NetworkCredential("admin", "123456");
client.Send(mailMsg);
2.2 HTML格式邮件发送代码
AlternateView htmlBody =
AlternateView.CreateAlternateViewFromString(htmlBodyContent, null, "text/html");
mailMsg.AlternateViews.Add(htmlBody);或者mailMsg.IsBodyHtml = true;
2.3 发送带嵌入图片邮件之SMTP实现和ESMTP实现
参考博文:http://www.cnblogs.com/wuhuacong/archive/2009/11/13/1601491.html
2.4 激活邮件发送
需求:用户注册以后不能正常使用等(暂时用一个激活才能看的页面测试),必须激活帐户以后才能正常使用。为了保证用户使用的是正确的邮箱地址,因此向用户的邮箱发送激活邮件。
于是通过学习,封装了如下一段发送验证邮件的代码:
public void SendValidateEmail(Guid userId)
{
MembershipUser user = Membership.GetUser(userId);
Users_Ext userInfo = GetByUserId(userId);
userInfo.VCode = Guid.NewGuid();
new Users_ExtBLL().Update(userInfo);
MailMessage mailMsg = new MailMessage();//两个类,别混了应该引入System.Net.Mail下的
mailMsg.From = new MailAddress(ConfigurationManager.AppSettings["SystemEmailAddress"].ToString(),
ConfigurationManager.AppSettings["SystemEmailName"].ToString());//源邮件地址
mailMsg.To.Add(new MailAddress(user.Email, user.UserName));//目的邮件地址。可以有多个收件人
mailMsg.Subject = "激活您的闪梦SNS网账户:" + user.UserName;//发送邮件的标题
string validateUrl = ConfigurationManager.AppSettings["ValidateEmailUrl"].ToString() +
"?username=" + HttpUtility.UrlEncode(user.UserName) + "&vcode=" + userInfo.VCode;
mailMsg.Body = "点击下面的链接激活您的账户(如果看不到如果看不到超链接,则把网址粘贴到您的浏览器打开)" +
"<a href='" + validateUrl + "'>点此激活</a>";//发送邮件的内容
mailMsg.IsBodyHtml = true;
SmtpClient client = new SmtpClient(ConfigurationManager.AppSettings["SMTPServer"].ToString());
client.Credentials = new NetworkCredential(ConfigurationManager.AppSettings["EmailLoginName"].ToString(),
ConfigurationManager.AppSettings["EmailLoginPwd"].ToString());
//有的smtp服务器的用户名是:yzk@rupeng.com,有的是yzk
//用户名、密码必须和From一致
client.Send(mailMsg);
}
由于刚注册的用户数据库中还木有验证码VCode,这里首先生成一个新的Guid来更新用户的VCode。然后将此VCode写到邮件中的验证URL(ValidateUrl)中。并且通过此VCode来判断是否激活相关用户。当然为了防止重复激活,需要设置一个状态,这里我设置了一个枚举,有两种状态:Activated和UnActivated。
2.5 找回密码功能
需求:用户输入用户名,如果用户名存在,则显示密码问题要求用户输入答案(思考为什么?),用户答案输入正确则随机生成一个密码(思考为什么不把旧密码发给用户),用新密码修改旧密码,并向用户名的邮箱发送新的密码,提示用户“新密码已经发送到你的a***@163.com的邮箱”。
要考虑的技术点包括:只有设置requiresQuestionAndAnswer=“true”以后ResetPassword才会检查密码问题;调用MembershipUser的ResetPassword方法来根据密码问题答案重置密码,如果答案错误,则抛出MembershipPasswordException, ResetPassword方法返回随机生成的密码,返回的密码太复杂,因此需要生成一个6位的随机密码(思考,参考备注),调用ChangePassword修改密码。如何把Email进行掩码处理,也就asfdasfd@163.com→a*****@163.com
此CMS实践中,主要依靠几步简单的流程来进行找回密码。第一步,首先输入用户名,由Membership API 判断用户名是否存在,存在则转到第二步;第二步,显示用户注册时输入的密码找回问题,然后由用户回答,回答正确则转到第三步;第三步,首先重置用户密码为新生成的Guid的后6位,然后通过邮件服务器Magic Winmail向用户邮箱发送一封邮件告知新密码。
(3)ASP.NET安全模型
如果只是配置哪些用户、角色才能访问某个页面等操作,不用调用API判断,只要配置Web.Config即可。在system.web节点下增加
<authorization>
<deny users="?"/>
</authorization>
deny代表禁用访问,?表示未验证用户,*表示所有用户。
还可以添加allow节点添加允许访问的条件,也可以添加多个deny、allow,这样按照从上向下匹配,第一个匹配的规则起作用。因此
<authorization>
<allow users=“admin,test1,test2”/>
<deny users=“*"/>
</authorization>
表示允许admin和test1,test2访问(多个用户名之间用逗号分隔),其他人不能访问。因为“第一个匹配的规则起作用”,因此不能颠倒顺序。
实际应用中很少根据用户名进行授权,一般是通过角色:<allow roles=“admin,editor”/>。
应用中很少针对整个网站做校验,Authorization默认作用域所在的文件夹及子文件夹,如果只想作用于某个页面或者文件夹,则使用location节点(注意添加在configuration节点下,也就是和system.web平级)
<location path="Account/WebForm1.aspx">
<system.web>
<authorization>
<allow users="yzk"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
如果安全配置不能满足灵活的业务需求,调用API即可,比如“admin在白天可以访问”。
在Roles角色类中常用的方法如下:
Roles.AddUsersToRole(,);添加多个用户到一个角色
Roles.CreateRole();//创建角色
Roles.DeleteRole();//删除角色
Roles.FindUsersInRole();//得到某个角色下的所有用户
Roles.GetAllRoles();//得到所有的角色
Roles.GetRolesForUser();//得到当前用户或者指定用户的所有角色
Roles.IsUserInRole();//判断用户是否属于某个角色
Roles.RemoveUserFromRole();//从角色中删除用户
Roles.RoleExists();//判断角色是否存在
(4)第三方集成支付
第三方支付平台就是提供网上支付的平台,由第三方支付平台来和各个银行进行对接,商户只要和第三方支付平台对接即可,降低了商户的技术难度和接入门槛。常见的第三方支付平台:支付宝、网银在线、快钱、财付通、易宝等。
一个支付流程的数据流动:客户在网上商店挑选商品、点击支付,网站将用户重定向到第三方支付平台的支付网关,并且将订单号、金额等信息通过QueryString传递给支付网关,用户在第三方支付平台支付成功后,第三方支付平台会自动访问商户的确认页面,将支付成功的订单号等信息通过QueryString传递给确认页面,这样商户网站就能得到支付成功的通知了。以服装卖场中的收银台流程类比(漏洞:自己偷偷盖假章,防范办法:收银台和商户约定一个密钥“天灵灵”,然后收银台在小票上根据“小票编号”+“金额”+密钥计算出md5写到小票上)。
重点:防止用户自己篡改“小票章”,这里支付宝和商家都事先采用一个密钥(假定为一个MD5值),双方通过QueryString中传递过来的参数使用MD5校验真伪。这里因为一些特定原因,无法使用真实的支付宝和网银来进行实践,于是可爱的老杨,邪恶的老杨给学子们开发了一个支付宝模拟器Simulator。这个Simulator采用了和支付宝与商家交流的原理来模拟支付流程,使我们能够进行相关的实践练习。注意:Callback页面的参数是支付宝传递过来的。
(5)无刷新上传
网站编辑编辑文章的时候需要插入图片、文件,如果使用FileUpload控件必须提交表单,则非常难用(很多文件)。由于AJAX不允许上传文件,所以必须用其他解决方案,Flash提供了异步上传文件的功能,因此用别人写好的Flash控件即可SWFUpload。
SWFUpload原理:Flash把文件异步上传到服务器端的程序(服务器端程序通过Filedata域的值得到上传的文件),上传成功把结果通过upload_success_handler事件通知给程序(第一个参数为上传的文件,第二个参数为服务器的返回内容)。服务器端程序和上传成功后的处理需要程序员编写。
(6)UBB编辑器集成
UBB代码是HTML的一个变种,是Ultimate Bulletin Board (国外的一个BBS程序)采用的一种特殊的TAG。您也许已经对它很熟悉了。UBB代码很简单,功能很少,但是由于其Tag语法检查实现非常容易,所以不少网站引入了这种代码,以方便网友使用显示图片/链接/加粗字体等常见功能。
这种代码使用正则表达式来进行匹配,不同的论坛所使用的UBB代码很可能不同,不能一概而论。UBB代码的出现,使得论坛可以使用类似HTML的标签来增加文字的属性,同时又不用害怕HTML代码中所夹带的不良信息!
此CMS系统实践中使用CKEditor(3.6版本后支持了UBB便器),数据库中保存的也是UBB内容,在显示出来的时候翻译成HTML代码。这里分享一个js代码:text2html,即将UBB格式代码转化为HTML格式:
function text2html(s)
{
if(s.indexOf("://") > 0)
{
//url
s = s.replace(/(^|[^\"\'\]])(http|ftp|mms|rstp|news|https)\:\/\/([^\s33\[\]\"\']+)/gi, "$1[url]$2://$3[/url]");
//img
s = s.replace(/\[url\](http\:\/\/\S+\.)(gif|jpg|jpeg|png)\[\/url\]/gi, "[img]$1$2[/img]");
}
//ubb: 匹配[UBB]...[/UBB]形式
if(s.match(/\[(\w+)([^\[\]\s]*)\].*\[\/\1\]/))
{
s = s.replace(/\[url\](.+?)\[\/url\]/gi,"<a href=$1 target=_blank>$1</a>");
s = s.replace(/\[img\](.+?\.(?:gif|jpg|jpeg|png))\[\/img\]/gi, "<img src='$1' alt='$1'>");
s = s.replace(/\[flash\](.+?\.swf)\[\/flash\]/gi, "<embed src='$1' quality=high wmode=transparent type='application/x-shockwave- flash' width=400 height=300></embed><br> FLASH: <a href='$1' target=_blank>$1</a><br>");
s = s.replace(/\[wma\](.+?\.(?:wma|mp3))\[\/wma\]/gi, "<embed src='$1' height=40 AutoStart=0></embed><br> WMA: <a href='$1' target=_blank>$1</a><br>");
s = s.replace(/\[color=([#0-9a-zA-Z]{1,10})\](.+?)\[\/color\]/gi, "<font color='$1'>$2</font>");
s = s.replace(/\[b\](.+?)\[\/b\]/gi, "<b>$1</b>");
s = s.replace(/\[i\](.+?)\[\/i\]/gi, "<i>$1</i>");
}
return s;
}
(7)URL重写
为什么要URL重写?
1、有利于SEO,带参数的URL由于内容可能是动态改变的,因此带参数的URL权重较低;2、地址看起来更正规。看DiscuzNT的URL重写。
区别于伪静态:伪静态页面看起来像普通页面,而非动态生成的页面。
经过调试发现,每个浏览器端的请求都会在Application_BeginRequest 中中触发,然后HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath 获得要访问资源的虚拟路径,哪怕访问一个服务器上不存在的页面, Application_BeginRequest 也会被调用。然后用HttpContext.Current.RewritePath(ReWriteUrl)进行重写(也就是交由另外一个页面处理这个请求)(ViewArticle.aspx?tid=3格式)
URL重写有利于SEO,带参数的URL由于内容可能是动态改变的,因此搜索引擎给带参数的URL权重可能会低。
在Global.asax的Application_BeginRequest 中读取Request.Url 得到请求的URL(View-3.aspx),然后用HttpContext.Current.RewritePath(ReWriteUrl)进行重写(View.aspx?tid=3格式)
context.Request.AppRelativeCurrentExecutionFilePath//用虚拟路径来判断更加科学。
context.RewritePath("~/Post/ViewArticle.aspx?tid="+id);
(8)页面静态化
静态化降低数据库、Web服务器CPU的压力,用户获得一个静态页面,服务器几乎不需要运算,甚至可以用负载均衡用专门的文件服务器存储静态页。
按照日期进行静态化存储。使用流水号生成器来生成序号,生成的静态文件名用单独字段保存。
保存文章的时候静态化生成,点击一个按钮全部重新生成。
使用静态化的时候需要注意相对路径的问题,图片、js、css等都用绝对路径。
新闻查看页面都是静态内容,不应该有服务端事件处理这些东西,请求htm的时候也不会执行aspx的Page_Load等事件。登录区域等都是ajax处理的。WebClient也是一个浏览器,是向aspx发请求获得html内容,不是下载aspx,而是下载aspx执行的结果生成的html。
因为评论的加载、提交、登录区域的处理都是AJAX的,没有post表单提交,所以页面静态化不影响页面的动态效果。
页面静态化和URL重写目的不一样,URL重写和页面静态化都可以SEO,但是URL重写SEO的开发难度小,页面静态化除了SEO之外还能大大降低服务器的压力。只有经常被访问的、很少变化的内容做成静态化。
下面是一段封装后的静态化页面代码:
public void StaticArticle(int articleId)
{
ArticleBLL artbll = new ArticleBLL();
var article = artbll.GetById(articleId);
string staticPath;
//如果StaticPath为空,则是第一次生成静态页
if (string.IsNullOrEmpty(article.StaticPath))
{
System.Data.SqlClient.SqlParameter pName = new System.Data.SqlClient.SqlParameter();
pName.SqlDbType = System.Data.SqlDbType.NVarChar;
pName.ParameterName = "@SeqName";
pName.Value = "静态页面流水号";
System.Data.SqlClient.SqlParameter pResult = new System.Data.SqlClient.SqlParameter();
pResult.SqlDbType = System.Data.SqlDbType.Int;
pResult.ParameterName = "@Result";
pResult.Direction = System.Data.ParameterDirection.Output;
//调用存储过程sp_getSeqNo生成流水号
DAL.SqlHelper.ExecuteStoredProcedure("sp_getSeqNo", pName, pResult);
int seqNo = (int)pResult.Value;
//要生成的静态页面的保存路径
staticPath = article.PostDate.ToString(@"yyyy\/MM\/dd") + seqNo + ".html";
//把生成的静态路径保存在数据库中
article.StaticPath = staticPath;
artbll.Update(article);
}
else
{
staticPath = article.StaticPath;
}
string localPath = HostingEnvironment.MapPath("~/HTML/" + staticPath);
//如果本地路径不存在则创建文件夹
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(localPath));
WebClient wc = new WebClient();
wc.Encoding = System.Text.Encoding.UTF8;
//通过WebClient向服务器发Get请求,把服务器返回的html内容保存到磁盘上—以后用户直接请html文件请求。
wc.DownloadFile("http://127.0.0.1:9145/Admin/ViewArticle.aspx?id=" + article.Id,
localPath);
}
(9)过滤词管理
为了保证网站的安全、防垃圾,要对用户的评论内容进行过滤。
创建过滤词表,FilterWords(Id,WordPattern(匹配词),ReplaceWord(替换词))
开发管理后台过滤词导入程序,“每行一组过滤词语,不良词语和替换词语之间使用“=”进行分割,禁用词的替换词语为“{BANNED}”,审核词的替换词语为“{MOD}” ”、对于已经存在的词汇处理策略有三种:使用新的设置覆盖已经存在的词语(用update或者先delete再insert);不导入已经存在的词语;清空当前词表后导入新词语。
过滤词导出到txt文件,弹出下载对话框。如果HttpHandler输出的是html、txt、jpeg等类型的信息,那么浏览器会直接显示,如果希望弹出保存对话框,则需要添加Header:string encodeFileName = HttpUtility.UrlEncode(“过滤词.txt”); Response.AddHeader(“Content-Disposition”, string.Format(“attachment;filename=\”{0}\“”, encodeFileName));其中filename后为编码后的文件名。filename段为建议的保存文件名。动态输出用处,不用再把资源保存到磁盘上再输出(不会有文件重名的问题,文件不生成在服务器端)。
快速判断评论是否含有禁用词判断:将禁用的词用"|"(表示或)进行分割得到表达式,用Regex进行匹配,如果Matches结果多于1,说明含有禁用词。如果含有禁用词就没必要后续处理。
用相似的方法判断是否需要审核,为评论表增加一个IsVisible字段,如果评论需要审核则设置IsVisible=false,否则IsVisible=true,只显示IsVisible=true的评论。
只要评论不含有禁用词,在将评论放入数据库之前将文章中出现的需要进行替换的词进行替换再保存到数据库中。批量进行字符替换用正则表达式的Replace方法:regexReplace.Replace(msg, new MatchEvaluator(ReplaceText))
要点:为了提高效率,采用缓存将正则表达式存储进去。不必每次都去耗时提取正则表达式来匹配,如果每次发帖都要从数据库中取过滤词,效率低。但是,当对过滤词进行管理的时候要清理过滤词缓存。
(10)基于Luence.NET的站内搜索
站内搜索模块:生产者、消费者,多线程。复习多线程,用多线程做了一个winform的生产者、消费者的例子,有任务的时候(点按钮给整数)就处理任务,没任务的时候就每次扫描都说“还是没任务,睡会再看”,线程中操作UI线程的代码见下面:
ParameterizedThreadStart threadStart = (obj) =>
{
txtLog.AppendText(obj + "\n");
};
txtLog.Invoke(threadStart, item);
由于索引库同时只能有一个IndexWriter进行写,所以有一个消费者线程一直保持对IndexWriter写的状态,有新任务进入的时候对IndexWriter写入。如果IndexWriter一直保持打开状态的话,新添加的文档是不会被搜索到的,因此必须处理完队列中的任务后关闭writer,然后下次while循环扫描的时候判断如果队列汇总没有任务,则sleep5秒钟后再判断,防止不断判断给服务器cpu压力
IndexManager做成单例。维持一个任务的List,Thread thread = new Thread(ScanThread); thread.Start();启动一个线程,在ScanThread方法中不断遍历List,当有新任务加入的时候把新任务加入索引库,当要删除文章的时候也是加入一个jobType == JobType.Delete的内容。
Pangu分词默认是尽可能的少分词,所以“计算机等级考试开始”会分成“计算机等级考试|开始”,这样搜“计算机”就搜不到。Todo:需要修正bug,多元分词也搜不到。需要解决问题
MultiDimensionality设置为true,开启多元分词,<Redundancy>2</Redundancy>设置多元分词冗余度。
假设索引的过程非常慢,通过把任务提交给线程去执行,好处就是“保存文章”操作不用等任务执行完毕才完成,而是立即返回,又线程去执行耗时的操作。
可以用到注册邮件的发送上,注册过程只要把“发给谁、内容是什么”提交给“发邮件的消费者就可以了”,由消费者去慢慢执行,程序立即返回。
下面是实践中的实例界面: