【IT168 专稿】在上篇中,我们以标签管理子模块和博客入口管理子模块为例探讨了系统中主要的管理页面设计的设计特征。在本系列这个最后一篇中,我们将讨论系统安全及错误处理模块设计的设计问题。
第一篇:基于ASP.NET 3.5与N层架构开发企业SNS
第二篇:用ASP.NET3.5开发SNS:用户接口页面设计
1.博客系统的安全性设计
直到现在,我们一点也没有讨论系统的安全性问题。但是,幸运的是,本博客系统的安全模型非常简单:
·仅有系统管理员可以使用系统中提供的一切管理功能。
·系统管理员数据定义和存储于本系统后台数据库的authors表格中。
·系统管理员在可以使用任何管理功能之前需要先登录并标识自己。一旦得到系统的认证,他们便可以使用系统中所有的功能。
(1)用户信息的安全存储
读者不难想像,实现这个模型并不困难。我们不妨先回顾一下作者(系统管理员)信息在博客系统中的管理方式。Author对象其实是很简单的:它们仅仅有一个用户名和一个口令。然而,以普通文本方式把口令存储到数据库中并不是明智的选择。这就是为什么我们在本系统中存储了口令的哈希值。通过对用户登录时的口令应用相同的哈希算法,我们便可以与数据库中存储的结果进行比较来判断是否用户提供了一个正确的口令。
但是,上面的安全措施仍然存在一定的脆弱性:如果有人能够设法访问到数据库并且看到了所有的哈希口令,那么他们可以运行一个暴力式字典攻击来把该口令通过逆向工程进行破解。通过为每一个哈希值提供一个唯一的“盐”(salt)值,便可以解决这个问题。
【注意】Salt,即“盐”,是哈希算法中的一个重要概念。实际开发中,服务器通常使用Hash算法(如sha1)把客户传过来的明文密码加密后,和用户名一起存入数据库。Salt是随机产生的字符串,与密码结合后,再加密,意在增加字典攻击的难度。
注意,生成盐化哈希值的逻辑编码位于Code/Utils/Hash.cs文件中。数据访问对象forAuthors的保存方法使用这一逻辑来存储用户。下面给出了文件Code/Data/Access/Author.db.cs中部分相关的代码片断:
t.salt = Hash.GenerateSalt(16);
t.password = Hash.HashPassword(myAuthor.password, t.salt);
最后,AdminAuthors.aspx页面和AdminAuthorEditNew.aspx页面能够使系统管理员管理所有的用户,具体实现细节读者可自行参考本文所附源码。
(2)认证问题
ASP.NET提供了多种不同的途径支持用户认证。通过前面的介绍,我们已经了解到我们使用了定制方式实现用户的管理。我们还不难得出这样的结论:既然本博客系统是一个因特网应用程序,那么这就排除了通过一个活动目录提供者使用Windows认证的选择。代之的是,我们将使用ASP.NET中的FormsAuthentication组件,并且还要使用一个定制的登录表单和成员身份提供者技术来实现用户认证。
首先,我们需要从配置文件web.config中启动表单认证支持,代码如下:
<forms loginUrl="~/View/Pages/Login.aspx"
protection="All"
timeout="30"
name="BlogoCookie"
requireSSL="false"
slidingExpiration="true"
defaultUrl="~/View/Pages/Admin/Admin.aspx"
cookieless="UseCookies"
enableCrossAppRedirects="false"/>
</authentication>
在文件web.config的上述配置节中:
·loginURL:指出我们的登录表单的地址。
·name:要使用的登录cookie的名称。
·defaultURL:一旦登录成功要导航到的地址。
接下来,我们在配置文件web.config中建立我们的定制MembershipProvider:
<providers>
<clear/>
<add name="BlogoMembershipProvider" applicationName="BLOGO.NET"
type="BLOGO.NET.Utils.BlogoMembershipProvider" />
</providers>
</membership>
上面的配置要求应用程序使用我们的定制的MembershipProvider组件—BlogoMembershipProvider。
最后,我们需要在配置文件web.config中建立一些授权规则。对于本系统而言,这一点非常简单:除了位于“View/Pages/Admin”文件夹中的页面,所有其他的页面都可以不经认证而进行访问。相关配置代码如下:
<location path="View/Pages/Admin">
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
在设置完web.config文件后,现在来进一步讨论我们的定制MembershipProvider,它位于文件Code/Utils/BlogoMembershipProvider.cs中。
这个定制MembershipProvider的目的是鉴别进入到本博客系统中的用户。为此,我们可以从ASP.NET的MembershipProvider基类派生我们自己的定制成员身份类。有许多种方法可以重载基类,但是我们将仅重载一个方法—ValidateUser。
下面给出重载基类MembershipProvider的方法ValidateUser的相关代码:
{
bool result = false;
try
{
if (AuthorManager.Count(0, 10) < 1 && username == "admin"
&& password == "admin")
{
result = true;
}
else
{
string salt = null;
string hashedPassword = null;
Author currentUser = AuthorManager.GetItem(username);
if (currentUser != null)
{
salt = currentUser.salt;
hashedPassword = Hash.HashPassword(password, salt);
if (hashedPassword.Equals(currentUser.password))
{
//认证成功
result = true;
}
}
}
}
catch (Exception)
{
}
return result;
}
上面重载方法代码的总体实现逻辑是:
·使用业务层的AuthorManager类计算出已定义的用户。如果不存在任何用户,则说明系统配置不当,因此允许使用默认的用户名和口令“admin”。如果至少定义了一个用户,则不接受“admin”,并且说明系统安全运行。
·根据提供的用户名从业务层中查找相应的Author对象。如果找到,则把当前提供的口令与存储在数据库中的经盐化的口令进行比较。如果口令匹配,则认证成功。
·如果不存在这样的用户,或如果数据库中的哈希值不相匹配,则认证失败.
在修改了配置文件web.config并创建了定制的MembershipProvider后,开发登录表单就很容易了。此表单位于View/Pages/Login.aspx路径下,其中仅包含下面的标记代码:
Inherits="BLOGO.NET.View.Pages.Login"
MasterPageFile="~/View/Masters/Site.Master" %>
<asp:content id="ContentPlaceHolder" contentplaceholderid="ContentPlaceHolderPrimary"
runat="server">
<form id="form" runat="server">
<div class="post">
<h3>访问管理面板登录</h3>
<asp:Login ID="LoginBlogo" runat="server"
MembershipProvider="BlogoMembershipProvider"
DisplayRememberMe="False"
RememberMeSet="False"
FailureText="无效的凭证。请重新输入!">
<LoginButtonStyle CssClass="button" />
</asp:Login>
<asp:Label ID="LabelMessage" runat="server" Text=""></asp:Label>
</div>
</form>
</asp:content>
此外,还请注意以下几点:
·该登录表单直接继承自站点母版页面。
·在这个页面上,使用了一个登录控件。它被配置使用我们的定制MembershipProvider。我们还配置了相应的错误文本以防发生认证失败.
·我们在登录表单的底部包括了一个标签控件。这个标签用于显示指令以防不存在用户定义。我们在表单的Page_Load事件处理器函数中填充这个标签中的文本。
经过上面简单的编程,现在我们实现一个强有力的定制的安全模型。所有这些主要得益于重用大量的ASP.NET标准设施:
·当用户访问任何访问者页面时,不出现登录表单。
·当用户访问任何管理页面时,他们首先被导航到登录页面(如果他们已经认证—这可以通过浏览器端的Cookie进行探测,并且跳过登录页面)。
·在该登录表单中,使用定制的Membership提供者检查凭证。
·一旦失败,便显示认证错误文本。
·一旦成功,该用户即被认证并且被重定向到最初想进入的页面。
在本博客系统中,我们将在管理页面的侧栏中显示用户的登录状态,同时还提供一个选项以便用户退出应用程序。
你可能还记得,管理侧栏定义于管理母版页面(View/Masters/Admin.Master)中。下列代码显示当前登录的用户名以及一个方便用户退出应用程序的选项:
<LoggedInTemplate>
您的登录名为:<asp:LoginName ID="LoginName" runat="server" />
<br />
(<a id="LinkLogout" href="~/View/Pages/Logout.aspx" runat="server">退出</a>)
</LoggedInTemplate>
<AnonymousTemplate>
您尚未登录
<br />
(<a id="LinkLogin" href="~/View/Pages/Login.aspx" runat="server">登录</a>)
</AnonymousTemplate>
</asp:LoginView>
注意,上面代码中的logout链接指向了页面Logout.aspx。这个页面也很简单,其任务仅仅是在该表单的Page_Load事件代码中清除当前认证,然后把用户重定向回登录表单:
{
FormsAuthentication.SignOut();
Response.Redirect("~/View/Pages/Login.aspx");
}
至此,我们完全实现了系统的安全模型建设部分。
2.系统中的错误处理
我们要考虑的最后一个问题是系统的错误处理问题。在本博客系统中,我们的错误处理模式遵循如下标准:
·捕获我们估计的可能出现的错误并作出任何可能的处理。这样的一个示例是,在把一个新的标签保存之前我们需要检查是否已经存在这样的标签。如果我们不执行这样的检查,数据库将返回一个错误。
·使用一个应用程序级别的错误处理器来捕获未预料的错误。这个错误处理器将记录下该错误的所有细节,但是仅显示一个友好的错误页面给用户。如果在记录错误的过程中发生了错误,那么,当即捕获此错误,只是不对之作进一步的处理;否则,我们将导致进入一个无穷的循环中。
下面,让我们具体看一下如何实现上述方案。
首先,我们需要一种方式用于在数据库存储错误消息。事实上,我们已经实现了这一步,因为我们的数据库模型中拥有一个log(日志)表格,而且我们还提供一个Log业务对象和LogManager业务对象管理员用于把消息存储到数据库。
接下来,应当在应用程序的全局文件Global.asax(位于应用程序的根文件夹下)中编写代码。这个文件中包含若干应用程序级的事件。不过,我们仅对Application_Error事件感兴趣。每次一个不可处理的异常发生时这个事件都将被触发。因此,在这个事件函数中放置我们的逻辑最合适:
{
try
{
Server.Transfer("~/View/Pages/Error.aspx");
}
catch (Exception)
{
//在错误处理期间发生错误,不进行任何处理
//,否则,将导航无限循环
}
}
注意,上面代码中还没有记录下任何错误。所有发生的一切仅仅是把用户重定向到错误页面Error.aspx。
下面,让我们来分析错误页面Error.aspx。这是一个非常简单的页面,其中仅包含了一些静态标记,以便告诉用户发生了一个错误。该页面直接继承自站点级别的母版页面Site.Master。把错误日志记录到数据库的实际操作发生在该页面的Page_Load事件中。在这个事件中,我们使用业务层的LogManager类来记录错误细节:
{
try
{
//把错误记录到数据库中
Log l = new Log();
l.date = System.DateTime.Now;
l.@event = Server.GetLastError().ToString();
LogManager.Save(l);
Server.ClearError();
}
catch (Exception)
{
//在错误处理期间发生错误。不作任何处理;否则,将导致进入无穷循环。
}
}
如你所见,我们使用Server.GetLastError()方法检索在数据库中发生的最后一个未处理的异常。
3.总结
至此,我们完全结束了博客系统案例的讨论。在本博客中,我们基于标准的企业应用开发中的N层架构进行系统的设计,实现了系统主页、博客入口及各种博客文章查看页面、文件页面、标签云页面等用户接口页面和标签管理、博客入口管理和用户认证及错误处理等基本模块。
时间所限,我们并没有针对所有页面实施AJAX修改方案,而且也没有考虑其他常见模块,如RSS信息聚合模块、各种统计模块、静态及动态广告模块等。
最后的感想是,借助一个合理的N层架构、标准化的功能强大的ASP.NET 3.5控件和以前积累的开发体验,我们就能够快速开发出体面而漂亮的Web应用程序,而且这样的程序易于以后的维护和扩展。