作者:Willmove && Heath Stewart
主頁:http://www.amuhouse.com
E-mail: willmove@Gmail.com
說明:兩個月前我剛學(xué) asp.net, 在 codePRoject.com 看到題目叫 Role-based Security with Forms Authentication 的文章,覺得很有幫助。當(dāng)時就想翻譯成中文。不過直接翻譯實(shí)在沒意思,這兩天我參照 Heath Stewart的這篇文章,并且根據(jù)自己的理解,把它按照自己的想法和表達(dá)方式寫成中文。附帶上自己為這篇文章做的一個演示的web應(yīng)用程序。
如果有理解錯誤的地方,歡迎來信指出或發(fā)表評論。
附:垃圾郵件實(shí)在討厭,請高抬貴手。
原文章在 http://www.codeproject.com/aspnet/formsroleauth.asp
原作者 Heath Stewart
概要:
ASP.NET 提供了基于角色(即 Roles)的認(rèn)證機(jī)制,然而它對角色的支持是不完全的。本文試圖通過一些例子來說明如何實(shí)現(xiàn)和使用這種基于角色的認(rèn)證機(jī)制。
簡介:
ASP.NET 中窗體認(rèn)證是一個功能非常強(qiáng)大的特性,只需要很少的代碼就可以實(shí)現(xiàn)一個簡單的平臺無關(guān)的安全認(rèn)證系統(tǒng)。
但是,如果你需要一個更復(fù)雜更有效的認(rèn)證機(jī)制,那么你就要把眾多用戶分成用戶群組,以利用它的靈活性。Windows 集成認(rèn)證提供了這種認(rèn)證機(jī)制,但它使用的是 NTLM,即Windows NT LAN Manager,因而它不是跨平臺的?,F(xiàn)在越來越多的人使用 linux 系統(tǒng),而 Mozilla Forefox 瀏覽器用戶也越來越多,我們肯定不能把這些人拒之門外,因此我們尋求另外的認(rèn)證機(jī)制。有兩個選擇:一是為網(wǎng)站劃分多個區(qū)域,提供多個登錄頁面,強(qiáng)迫用戶一個一個的去注冊和登錄;二是把用戶分組,并且限制特定用戶組對某頁面或者某區(qū)域訪問的權(quán)限。后者當(dāng)然是更好的選擇。通過分配角色給各個用戶,我們能夠?qū)崿F(xiàn)這種功能。
微軟為.NET平臺留下了窗體認(rèn)證中基于角色的認(rèn)證機(jī)制,但是我們必須自己去實(shí)現(xiàn)它。本文力求覆蓋窗體認(rèn)證中基于角色的認(rèn)證機(jī)制的一些基本的東西,比如它的概念,它的實(shí)現(xiàn),如何在Web應(yīng)用程序中應(yīng)用等。
必要準(zhǔn)備:
我們首先要建立一個數(shù)據(jù)庫,一個Web應(yīng)用項目,幾個不同安全級別的機(jī)密目錄,以及幾個ASP.NET頁面。當(dāng)然你也可以在你現(xiàn)有的Web應(yīng)用項目中添加這些。
1、創(chuàng)建數(shù)據(jù)庫
首先要選擇你需要使用的數(shù)據(jù)庫管理系統(tǒng) DBMS。本文使用 SQL Server 2000。
在實(shí)際應(yīng)用項目的數(shù)據(jù)庫中,一般都會有用戶數(shù)據(jù)表 Users,它可能包括用戶唯一標(biāo)記:UserID,用戶名:UserName,密碼:PassWord,用戶的郵件地址:Email,用戶所在城市:City,用戶登錄次數(shù) LoginCount 等。可以通過創(chuàng)建一個 UserInRoles 數(shù)據(jù)表(一般可以包括兩個字段,用戶名:UserName,用戶角色:UserRoles)來實(shí)現(xiàn)為用戶分配角色。
為了簡單,我只創(chuàng)建一個 Users 數(shù)據(jù)表,它有3個字段,用戶名 UserName,密碼 Password,用戶角色 UserRoles。創(chuàng)建表之前,你要選擇數(shù)據(jù)庫,或者創(chuàng)建一個新的數(shù)據(jù)庫。要創(chuàng)建一個新的命名為WebSolution的數(shù)據(jù)庫 ,只需要簡單的SQL語句:
程序代碼
Create DATABASE WebSolution
GO
要選擇一個叫msdb的數(shù)據(jù)庫,可以使用SQL語句:
程序代碼
USE msdb
GO
接下來,我們創(chuàng)建剛才提到的 Users 數(shù)據(jù)表,SQL 腳本如下:
程序代碼
Create TABLE Users
(
UserName nvarchar(100) CONSTRAINT PK_UserName PRIMARY KEY,
Password nvarchar(150),
UserRoles nvarchar(100)
)
可以為這個表創(chuàng)建索引 Credentials,SQL語句如下:
程序代碼
Create INDEX Credentials ON Users
(
UserName,
Password
)
是否創(chuàng)建索引是可選的,由你自己決定。索引的好處和壞處請參考相關(guān)資料。
然后我們?yōu)檫@個Users數(shù)據(jù)庫添加數(shù)據(jù)。角色名稱由你自己自由選擇,但是最好用有意義的名稱,比如
"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 文件中是大小寫敏感的。現(xiàn)在我們?yōu)閷?shí)現(xiàn)這個安全認(rèn)證機(jī)制創(chuàng)建幾個必要的頁面。
首先是用戶登錄頁面 Login.aspx
如果還沒有創(chuàng)建Web應(yīng)用程序,那就現(xiàn)在創(chuàng)建一個。當(dāng)然你也可以在一個已有的Web應(yīng)用程序中創(chuàng)建這個頁面。這里我假設(shè)已經(jīng)創(chuàng)建了一個名稱為 RolebasedAuth的Web應(yīng)用程序(即 Visual Studio .Net 中的Project)。我把這個Login.aspx放在它的根目錄下,也就是通過 http://localhost/RolebasedAuth/Login.aspx 可以訪問。
這個Login.aspx放在哪里是無所謂的,但是它必須是公眾有權(quán)限訪問的。
在應(yīng)用程序根路徑下,我們創(chuàng)建兩個機(jī)密的子目錄,分別是 Admin 和 User。
接下來,我們創(chuàng)建一個支持角色認(rèn)證的窗體認(rèn)證登錄系統(tǒng)。因為微軟沒有提供簡單的實(shí)現(xiàn)機(jī)制,我們要自己花些時間去創(chuàng)建認(rèn)證票據(jù)。它需要存貯少量信息,當(dāng)然,有些名稱必須和 Web.config 中配置的一樣,要不ASP.NET 就會認(rèn)為你的認(rèn)證票據(jù)是無效的,從而強(qiáng)制轉(zhuǎn)向到登錄頁面。我們在 VS.NET 中為 Login.aspx 添加兩個TextBox控件,取名 UserNameTextBox, PasswordTextBox,再添加一個Button,取名 LoginButton,點(diǎn)擊它進(jìn)入后臺代碼。在 LoginButton_Click 方法中添加需要的代碼。如下:
程序代碼
private void LoginButton_Click(object sender, System.EventArgs e)
{
// 初始化 FormsAuthentication
// 注意它是在 System.Web.Security 命名空間
// 因此要在代碼開始添加 using System.Web.Security;
FormsAuthentication.Initialize ();
// 創(chuàng)建數(shù)據(jù)庫連接和數(shù)據(jù)庫操作命令對象
// 注意它是在 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 UserName=@username " +
"AND Password=@password";
// 填充各個參數(shù)
cmd.Parameters.Add("@username", SqlDbType.NVarChar, 100).Value =
UserNameTextBox.Text;
cmd.Parameters.Add("@password", SqlDbType.NVarChar, 150).Value =
FormsAuthentication.HashPasswordForStoringInConfigFile(
PasswordTextBox.Text, "md5"); // 或者 "sha1"
// 執(zhí)行數(shù)據(jù)庫操作命令
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
// 為了實(shí)現(xiàn)認(rèn)證,創(chuàng)建一個新的票據(jù)
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // 票據(jù)版本號
UserNameTextBox.Text, // 票據(jù)持有者
DateTime.Now, //分配票據(jù)的時間
DateTime.Now.AddMinutes(30), // 失效時間
true, // 需要用戶的 cookie
reader.GetString(0), // 用戶數(shù)據(jù),這里其實(shí)就是用戶的角色
FormsAuthentication.FormsCookiePath);//cookie有效路徑
//使用機(jī)器碼machine key加密cookie,為了安全傳送
string hash = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName, // 認(rèn)證cookie的名稱
hash); //加密之后的cookie
//將cookie的失效時間設(shè)置為和票據(jù)tikets的失效時間一致
if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;
//添加cookie到頁面請求響應(yīng)中
Response.Cookies.Add(cookie);
// 將用戶轉(zhuǎn)向到之前請求的頁面,
// 如果之前沒有請求任何頁面,就轉(zhuǎn)向到首頁
string returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl == null) returnUrl = "./";
// 不要調(diào)用 FormsAuthentication.RedirectFromLoginPage 方法,
// 因為它會把剛才添加的票據(jù)(cookie)替換掉
Response.Redirect(returnUrl);
}
else
{
// 不要告訴用戶"密碼錯誤",這樣等于給了入侵者一個機(jī)會,
// 因為他們知道了他們輸入的用戶名是存在的
//
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_defaultClientScrjavaScript">
<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>
你會注意到上面我們對密碼的處理:將它哈希加密。哈希加密是一種單向算法(不可逆算法),生成唯一的字符數(shù)組。因此即使是改變密碼中一個字母的大小寫,都會生成完全不同的哈希列。我們把這些加密的密碼存儲在數(shù)據(jù)庫中,這樣更安全。在實(shí)際應(yīng)用中,你可能想為用戶找回忘記的密碼。但是哈希散列是不可逆的,所以你就不可能恢復(fù)原來的密碼。但是你可以更改用戶的密碼,并且把這個更改后的密碼告訴他。如果一個網(wǎng)站能夠給你舊密碼,那么你要考慮清楚了,你的用戶數(shù)據(jù)是不安全的!事實(shí)上,國內(nèi)大部分網(wǎng)站都是沒有經(jīng)過加密直接把用戶的密碼存儲到數(shù)據(jù)庫中的。如何一個黑客入侵成功,那么這些用戶帳戶就很危險了!
如果沒有使用SSL,你的密碼在網(wǎng)絡(luò)中也是以明文傳輸?shù)?。傳輸過程中可能會被竊取。在服務(wù)器端加密密碼只能保證密碼存儲的安全。SSL相關(guān)的資料可以在 http://www.versign.com 或 http://www.thewte.com 中找到。
如果你不想以加密方式在數(shù)據(jù)庫中存儲密碼,你可以更改上面的代碼,把
FormsAuthentication.HashPasswordForStoringInConfigFile(PasswordTextBox.Text, "md5") 改成 PasswordTextBox.Text 即可。
下一步,我們需要修改 Global.asax 文件。如果你的Web應(yīng)用程序沒有這個文件,請右鍵單擊Web應(yīng)用項目,選擇 "添加->添加新項...->Global application Class"。在 Global.asax 或者 Global.asax.cs 中,找到叫做 Application_AuthenticationRequest 的方法(函數(shù))。先要確認(rèn)已經(jīng)包含或者使用了 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;
// 取存儲在票據(jù)中的用戶數(shù)據(jù),在這里其實(shí)就是用戶的角色
string userData = ticket.UserData;
string[] roles = userData.Split(',');
HttpContext.Current.User = new GenericPrincipal(id, roles);
}
}
}
}
認(rèn)證票據(jù)(用戶名和密碼)是沒有作為cookie的一部分來存儲的,而且也不可以,因為用戶可以修改他們的cookie。
事實(shí)上,F(xiàn)ormsAuthentication是用你的機(jī)器碼 (machine key,通常在 machine.config 中)來加密票據(jù)(FormsAuthenticationTicket)的。我們使用 UserData 存儲用戶角色,并且生成一個新的憑證。一旦憑證已經(jīng)創(chuàng)建,它會被添加到當(dāng)前上下文中(即 HttpContext),這樣就可以用它來取回用戶角色了。
接下來,我們設(shè)置機(jī)密目錄(也就是"安全目錄",特定的使用者如管理員才有權(quán)限訪問的目錄)。首先看看你的Web應(yīng)用程序根目錄下是否有 Web.config 這個文件,如果沒有就創(chuàng)建一個。你也可以在你的子目錄中創(chuàng)建 Web.config 文件,當(dāng)然,這個 Web.config 文件是有限制的(一些參數(shù)它不可以設(shè)置)。要實(shí)現(xiàn)安全認(rèn)證,在 Web應(yīng)用程序根目錄下的 Web.config 文件中找到 <system.web> 節(jié)點(diǎn)下的
程序代碼
<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 這個名稱是任意的。要控制用戶或者用戶組的權(quán)限,我們可以有兩種方法,一是配置在應(yīng)用程序根目錄下的 Web.config 文件,二是在機(jī)密目錄下創(chuàng)建一個獨(dú)立的 Web.config 文件。(后者也許會比較好。)如果是前者,這個Web.config 就應(yīng)該包含有下面的內(nèi)容(或者類似的內(nèi)容):
程序代碼
<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應(yīng)用程序的目錄之前不互相依賴,可以比較方便的改名或者移動,可以選擇在每一個安全子目錄下配置單獨(dú)的 Web.config 文件。它只需要配置 <authorization/>節(jié)點(diǎn),如下:
程序代碼
<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="*" />
至此,我們已經(jīng)為網(wǎng)站配置了基于角色的安全認(rèn)證機(jī)制了。你可以先編譯你的程序,然后嘗試訪問一個機(jī)密目錄,例如 http://localhost/RolebasedAuth/Admin ,這時候你就會被轉(zhuǎn)向到用戶登錄頁面。如果你登錄成功,并且你的角色對這個目錄有訪問權(quán)限,你就重新回到這個目錄下??赡軙杏脩簦ɑ蛉肭终撸┢髨D進(jìn)入機(jī)密目錄,我們可以使用一個 session 來存儲用戶登錄的次數(shù),超過一定次數(shù)就不讓用戶登錄,并且顯示"系統(tǒng)拒絕了你的登錄請求!"。
下面,我們討論如何根據(jù)用戶角色讓W(xué)eb控件顯示不同內(nèi)容。
有時候根據(jù)用戶的角色來顯示內(nèi)容比較好,因為你可能不想為那么多不同的角色(用戶群組)制作一大堆有許多重復(fù)內(nèi)容的頁面。這樣的網(wǎng)站,各種用戶帳戶可以并存,付費(fèi)的用戶帳戶能夠訪問附加的付費(fèi)內(nèi)容。另一個例子是一個頁面將顯示一個 "進(jìn)入后臺管理" 按鈕鏈接到后臺管理頁面如果當(dāng)前用戶是 "Administrator"(高級管理員)角色。我們現(xiàn)在就實(shí)現(xiàn)這個頁面。
我們上面用到的 GenericPrincipal 類實(shí)現(xiàn)了 IPincipal 接口,這個接口有一個方法名叫做 IsInRole(),它的參數(shù)是一個字符串,這個字符串就是要驗證的用戶角色。如果我們要顯示內(nèi)容給角色是 "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>歡迎來到阿木小屋 http://amuhouse.com/ ^_^</p>
<asp:HyperLink id="AdminLink" runat="server"
Text="管理首頁" NavigateUrl="./Admin"/>
</body>
</html>
這樣,鏈接到 Admin 目錄的HyperLink 控件只會顯示給角色是 Administrator 的用戶。你也可以根據(jù)為未登錄用戶提供一個鏈接到登錄頁面,如:
程序代碼
protected void Page_Load(object sender, System.EventArgs e)
{
if (User.IsInRole("Administrator"))
{
AdminLink.Text = "管理員請進(jìn)";
AdminLink.NavigateUrl="./Admin";
}
else if(User.IsInRole("User"))
{
AdminLink.Text = "注冊用戶請進(jìn)";
AdminLink.NavigateUrl="./User";
}
else
{
AdminLink.Text = "請登錄";
AdminLink.NavigateUrl="Login.aspx?ReturnUrl=" + Request.Path;
}
}
這里,我們通過設(shè)置叫做ReturnUrl的 QueryString 變量,可以使用戶登錄成功后返回到當(dāng)前的這個頁面.
小結(jié):
本文用于幫助你理解基于角色安全機(jī)制的重要性、實(shí)用性,并且也用 ASP.NET 實(shí)現(xiàn)了基于角色的安全機(jī)制。它并不是一個很難實(shí)現(xiàn)的機(jī)制,不過它可能需要一些相關(guān)知識如 什么是用戶憑證,如何認(rèn)證用戶身份,以及如何審定授權(quán)用戶。如果你覺得它很有幫助,我會非常高興。我希望它可以引導(dǎo)你在你的網(wǎng)站中去實(shí)現(xiàn)基于角色的窗體安全認(rèn)證機(jī)制。
附:
本文的示例項目源代碼:
http://www.amuhouse.com/blog/upload/RolebasedAuth.rar
新聞熱點(diǎn)
疑難解答