asp.net|安全
说明:两个月前我刚学 ASP.NET, 在 codeproject.com 看到题目叫 Role-based Security with Forms Authentication 的文章,觉得很有帮助。当时就想翻译成中文。不过直接翻译实在没意思,这两天我参照 Heath Stewart的这篇文章,并且根据自己的理解,把它按照自己的想法和表达方式写成中文。附带上自己为这篇文章做的一个演示的web应用程序。
如果有理解错误的地方,欢迎来信指出或发表评论。
概要:
ASP.NET 提供了基于角色(即 Roles)的认证机制,然而它对角色的支持是不完全的。本文试图通过一些例子来说明如何实现和使用这种基于角色的认证机制。
简介:
ASP.NET 中窗体认证是一个功能非常强大的特性,只需要很少的代码就可以实现一个简单的平台无关的安全认证系统。
但是,如果你需要一个更复杂更有效的认证机制,那么你就要把众多用户分成用户群组,以利用它的灵活性。Windows 集成认证提供了这种认证机制,但它使用的是 NTLM,即Windows NT LAN Manager,因而它不是跨平台的。现在越来越多的人使用 Linux 系统,而 Mozilla Forefox 浏览器用户也越来越多,我们肯定不能把这些人拒之门外,因此我们寻求另外的认证机制。有两个选择:一是为网站划分多个区域,提供多个登录页面,强迫用户一个一个的去注册和登录;二是把用户分组,并且限制特定用户组对某页面或者某区域访问的权限。后者当然是更好的选择。通过分配角色给各个用户,我们能够实现这种功能。
微软为.NET平台留下了窗体认证中基于角色的认证机制,但是我们必须自己去实现它。本文力求覆盖窗体认证中基于角色的认证机制的一些基本的东西,比如它的概念,它的实现,如何在Web应用程序中应用等。
必要准备:
我们首先要建立一个数据库,一个Web应用项目,几个不同安全级别的机密目录,以及几个ASP.NET页面。当然你也可以在你现有的Web应用项目中添加这些。
1、创建数据库
首先要选择你需要使用的数据库管理系统 DBMS。本文使用 SQL Server 2000。
在实际应用项目的数据库中,一般都会有用户数据表 Users,它可能包括用户唯一标记:UserID,用户名:UserName,密码:Password,用户的邮件地址:Email,用户所在城市:City,用户登录次数 LoginCount 等。可以通过创建一个 UserInRoles 数据表(一般可以包括两个字段,用户名:UserName,用户角色:UserRoles)来实现为用户分配角色。
为了简单,我只创建一个 Users 数据表,它有3个字段,用户名 UserName,密码 Password,用户角色 UserRoles。创建表之前,你要选择数据库,或者创建一个新的数据库。要创建一个新的命名为WebSolution的数据库 ,只需要简单的SQL语句:
Create DATABASE WebSolution
GO
要选择一个叫msdb的数据库,可以使用SQL语句:
USE msdb
GO
接下来,我们创建刚才提到的 Users 数据表,SQL 脚本如下:
Create TABLE Users
(
UserName nvarchar(100) CONSTRAINT PK_UserName PRIMARY KEY,
Password nvarchar(150),
UserRoles nvarchar(100)
)
可以为这个表创建索引 Credentials,SQL语句如下:
Create INDEX Credentials ON Users
(
UserName,
Password
)
是否创建索引是可选的,由你自己决定。索引的好处和坏处请参考相关资料。
然后我们为这个Users数据库添加数据。角色名称由你自己自由选择,但是最好用有意义的名称,比如"Administrator"(顶级管理员),"Manager"(管理员),"Member"(加盟成员),"User"(普通用户)等。例如:
UserName|Password|Roles
"willmove"|"pwd123"|"Administrator,User"
"amuhouse"|"pwd123"|"User"
其SQL语句是:
--注意 '45CB41B32DCFB917CCD8614F1536D6DA' 是 'pwd123' 使用 md5 加密后的字符串
Insert INTO Users(UserName,Password,UserRoles) VALUES ('willmove','45CB41B32DCFB917CCD8614F1536D6DA','Administrator,User')
GO
Insert INTO Users(UserName,Password,UserRoles) VALUES ('amuhouse','45CB41B32DCFB917CCD8614F1536D6DA','User')
GO
要注意的是角色 Roles 是大小写敏感的,这是因为在 Web.config 文件中是大小写敏感的。现在我们为实现这个安全认证机制创建几个必要的页面。
首先是用户登录页面 Login.aspx
如果还没有创建Web应用程序,那就现在创建一个。当然你也可以在一个已有的Web应用程序中创建这个页面。这里我假设已经创建了一个名称为 RolebasedAuth的Web应用程序(即 Visual Studio .Net 中的Project)。我把这个Login.aspx放在它的根目录下,也就是通过 可以访问。
这个Login.aspx放在哪里是无所谓的,但是它必须是公众有权限访问的。
在应用程序根路径下,我们创建两个机密的子目录,分别是 Admin 和 User。
接下来,我们创建一个支持角色认证的窗体认证登录系统。因为微软没有提供简单的实现机制,我们要自己花些时间去创建认证票据。它需要存贮少量信息,当然,有些名称必须和 Web.config 中配置的一样,要不ASP.NET 就会认为你的认证票据是无效的,从而强制转向到登录页面。我们在 VS.NET 中为 Login.aspx 添加两个TextBox控件,取名 UserNameTextBox, PasswordTextBox,再添加一个Button,取名 LoginButton,点击它进入后台代码。在 LoginButton_Click 方法中添加需要的代码。如下:
private void LoginButton_Click(object sender, System.EventArgs e)
{
// 初始化 FormsAuthentication
// 注意它是在 System.Web.Security 命名空间
// 因此要在代码开始添加 using System.Web.Security;
FormsAuthentication.Initialize ();
// 创建数据库连接和数据库操作命令对象
// 注意它是在 System.Data.SqlClient 命名空间
// 因此要在代码开始处添加 using System.Data.SqlClient;
SqlConnection conn =
new SqlConnection("Data Source=sun-willmove;integrated security=SSPI;Initial Catalog=WebSolution;");
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "Select UserRoles FROM Users Where " +
"AND ";
// 填充各个参数
cmd.Parameters.Add("username", SqlDbType.NVarChar, 100).Value =
UserNameTextBox.Text;
cmd.Parameters.Add("password", SqlDbType.NVarChar, 150).Value =
FormsAuthentication.HashPasswordForStoringInConfigFile(
PasswordTextBox.Text, "md5"); // 或者 "sha1"
// 执行数据库操作命令
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
// 为了实现认证,创建一个新的票据
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // 票据版本号
UserNameTextBox.Text, // 票据持有者
DateTime.Now, //分配票据的时间
DateTime.Now.AddMinutes(30), // 失效时间
true, // 需要用户的 cookie
reader.GetString(0), // 用户数据,这里其实就是用户的角色
FormsAuthentication.FormsCookiePath);//cookie有效路径
//使用机器码machine key加密cookie,为了安全传送
string hash = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName, // 认证cookie的名称
hash); //加密之后的cookie
//将cookie的失效时间设置为和票据tikets的失效时间一致
if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;
//添加cookie到页面请求响应中
Response.Cookies.Add(cookie);
// 将用户转向到之前请求的页面,
// 如果之前没有请求任何页面,就转向到首页
string returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl == null) returnUrl = "./";
// 不要调用 FormsAuthentication.RedirectFromLoginPage 方法,
// 因为它会把刚才添加的票据(cookie)替换掉
Response.Redirect(returnUrl);
}
else
{
// 不要告诉用户"密码错误",这样等于给了入侵者一个机会,
// 因为他们知道了他们输入的用户名是存在的
//
ErrorLabel.Text = "用户名或者密码错误,请重试!";
ErrorLabel.Visible = true;
}
reader.Close();
conn.Close();
}
前台 aspx 页面代码如下:
<% Page language="c#" Codebehind="Login.aspx.cs" AutoEventWireup="false" Inherits="RolebasedAuth.Login" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>Login</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content=" ">
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<P>
<asp:Label id="Label1" runat="server">用户名:</asp:Label>
<asp:TextBox id="UserNameTextBox" runat="server"></asp:TextBox></P>
<P><FONT face="宋体"> </FONT>
<asp:Label id="Label2" runat="server">密码:</asp:Label>
<asp:TextBox id="PasswordTextBox" runat="server" TextMode="Password"></asp:TextBox></P>
<P>
<asp:Label id="ErrorLabel" runat="server" Visible="False"></asp:Label></P>
<P>
<asp:Button id="LoginButton" runat="server" Text="登录"></asp:Button></P>
</form>
</body>
</HTML>
你会注意到上面我们对密码的处理:将它哈希加密。哈希加密是一种单向算法(不可逆算法),生成唯一的字符数组。因此即使是改变密码中一个字母的大小写,都会生成完全不同的哈希列。我们把这些加密的密码存储在数据库中,这样更安全。在实际应用中,你可能想为用户找回忘记的密码。但是哈希散列是不可逆的,所以你就不可能恢复原来的密码。但是你可以更改用户的密码,并且把这个更改后的密码告诉他。如果一个网站能够给你旧密码,那么你要考虑清楚了,你的用户数据是不安全的!事实上,国内大部分网站都是没有经过加密直接把用户的密码存储到数据库中的。如何一个黑客入侵成功,那么这些用户帐户就很危险了!
如果没有使用SSL,你的密码在网络中也是以明文传输的。传输过程中可能会被窃取。在服务器端加密密码只能保证密码存储的安全。SSL相关的资料可以在 或 中找到。
如果你不想以加密方式在数据库中存储密码,你可以更改上面的代码,把
FormsAuthentication.HashPasswordForStoringInConfigFile(PasswordTextBox.Text, "md5") 改成 PasswordTextBox.Text 即可。
下一步,我们需要修改 Global.asax 文件。如果你的Web应用程序没有这个文件,请右键单击Web应用项目,选择 "添加->添加新项...->Global Application Class"。在 Global.asax 或者 Global.asax.cs 中,找到叫做 Application_AuthenticationRequest 的方法(函数)。先要确认已经包含或者使用了 System.Security.Principal 以及 System.Web.Security 命名空间,然后修改它,修改后的代码:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id =
(FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
// 取存储在票据中的用户数据,在这里其实就是用户的角色
string userData = ticket.UserData;
string[] roles = userData.Split(',');
HttpContext.Current.User = new GenericPrincipal(id, roles);
}
}
}
}
认证票据(用户名和密码)是没有作为cookie的一部分来存储的,而且也不可以,因为用户可以修改他们的cookie。
事实上,FormsAuthentication是用你的机器码 (machine key,通常在 machine.config 中)来加密票据(FormsAuthenticationTicket)的。我们使用 UserData 存储用户角色,并且生成一个新的凭证。一旦凭证已经创建,它会被添加到当前上下文中(即 HttpContext),这样就可以用它来取回用户角色了。
接下来,我们设置机密目录(也就是"安全目录",特定的使用者如管理员才有权限访问的目录)。首先看看你的Web应用程序根目录下是否有 Web.config 这个文件,如果没有就创建一个。你也可以在你的子目录中创建 Web.config 文件,当然,这个 Web.config 文件是有限制的(一些参数它不可以设置)。要实现安全认证,在 Web应用程序根目录下的 Web.config 文件中找到 <system.web> 节点下的
<authentication mode="Windows" />,把它修改为
<authentication mode="Forms">
<forms name="AMUHOUSE.ASPXAUTH"
loginUrl="Login.aspx"
protection="All"
path="./" />
</authentication>
<authorization>
<allow users="*"/>
</authorization>
上面的 name="AMUHOUSE.ASPXAUTH" 中,AMUHOUSE.ASPXAUTH 这个名称是任意的。要控制用户或者用户组的权限,我们可以有两种方法,一是配置在应用程序根目录下的 Web.config 文件,二是在机密目录下创建一个独立的 Web.config 文件。(后者也许会比较好。)如果是前者,这个Web.config 就应该包含有下面的内容(或者类似的内容):
<configuration>
<system.web>
<authentication mode="Forms">
<forms name=" AMUHOUSE.ASPXAUTH"
loginUrl="login.aspx"
protection="All"
path="/"/>
</authentication>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
<location path="./Admin">
<system.web>
<authorization>
<!-- 注意!下面几行的顺序和大小写是非常重要的! -->
<allow roles="Administrator"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
<location path="./User">
<system.web>
<authorization>
<!-- 注意!下面几行的顺序和大小写是非常重要的! -->
<allow roles="User"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
</configuration>
为了使Web应用程序的目录之前不互相依赖,可以比较方便的改名或者移动,可以选择在每一个安全子目录下配置单独的 Web.config 文件。它只需要配置 <authorization/>节点,如下:
<configuration>
<system.web>
<authorization>
<!-- 注意!下面几行的顺序和大小写是非常重要的! -->
<allow roles="Administrator"/>
<deny users="*"/>
</authorization>
</system.web>
</configuration>
需要再次提醒的是,上面的角色 roles 是大小写敏感的,为了方便,你也可以把上面修改为:
<allow roles="Administrator,administrator" />
如果你想允许或者禁止多个角色对这个目录的访问,可以用逗号隔开,如:
<allow roles="Administrator,Member,User" />
<deny users="*" />
至此,我们已经为网站配置了基于角色的安全认证机制了。你可以先编译你的程序,然后尝试访问一个机密目录,例如 ,这时候你就会被转向到用户登录页面。如果你登录成功,并且你的角色对这个目录有访问权限,你就重新回到这个目录下。可能会有用户(或入侵者)企图进入机密目录,我们可以使用一个 Session 来存储用户登录的次数,超过一定次数就不让用户登录,并且显示"系统拒绝了你的登录请求!"。
下面,我们讨论如何根据用户角色让Web控件显示不同内容。
有时候根据用户的角色来显示内容比较好,因为你可能不想为那么多不同的角色(用户群组)制作一大堆有许多重复内容的页面。这样的网站,各种用户帐户可以并存,付费的用户帐户能够访问附加的付费内容。另一个例子是一个页面将显示一个 "进入后台管理" 按钮链接到后台管理页面如果当前用户是 "Administrator"(高级管理员)角色。我们现在就实现这个页面。
我们上面用到的 GenericPrincipal 类实现了 IPincipal 接口,这个接口有一个方法名叫做 IsInRole(),它的参数是一个字符串,这个字符串就是要验证的用户角色。如果我们要显示内容给角色是 "Administrator"的已登录用户,我们可以在 Page_Load 中添加下面代码: 程序代码
if (User.IsInRole("Administrator"))
AdminLink.Visible = true;
整个的页面代码如下(为了简便,把后台代码也写在aspx页面): 程序代码
<html>
<head>
<title>欢迎您!</title>
<script runat="server">
protected void Page_Load(Object sender, EventArgs e)
{
if (User.IsInRole("Administrator"))
AdminLink.Visible = true;
else
AdminLink.Visible = false;
}
</script>
</head>
<body>
<h2>欢迎!</h2>
<p>欢迎来到阿木小屋 ^_^</p>
<asp:HyperLink id="AdminLink" runat="server"
Text="管理首页" NavigateUrl="./Admin"/>
</body>
</html>
样,链接到 Admin 目录的HyperLink 控件只会显示给角色是 Administrator 的用户。你也可以根据为未登录用户提供一个链接到登录页面,如:程序代码
protected void Page_Load(object sender, System.EventArgs e)
{
if (User.IsInRole("Administrator"))
{
AdminLink.Text = "管理员请进";
AdminLink.NavigateUrl="./Admin";
}
else if(User.IsInRole("User"))
{
AdminLink.Text = "注册用户请进";
AdminLink.NavigateUrl="./User";
}
else
{
AdminLink.Text = "请登录";
AdminLink.NavigateUrl="Login.aspx?ReturnUrl=" + Request.Path;
}
}
这里,我们通过设置叫做ReturnUrl的 QueryString 变量,可以使用户登录成功后返回到当前的这个页面.
小结:
本文用于帮助你理解基于角色安全机制的重要性、实用性,并且也用 ASP.NET 实现了基于角色的安全机制。它并不是一个很难实现的机制,不过它可能需要一些相关知识如 什么是用户凭证,如何认证用户身份,以及如何审定授权用户。如果你觉得它很有帮助,我会非常高兴。我希望它可以引导你在你的网站中去实现基于角色的窗体安全认证机制。