作者:Willmove && Heath Stewart
主頁:http://www.amuhouse.com
E-mail: [email protected]
說明:兩個月前我剛學 ASP.Net, 在 codeproject.com 看到題目叫 Role-based Security with Forms Authentication 的文章,覺得很有幫助。當時就想翻譯成中文。不過直接翻譯實在沒意思,這兩天我參照 Heath Stewart的這篇文章,並且根據自己的理解,把它按照自己的想法和表達方式寫成中文。附帶上自己為這篇文章做的一個演示的web應用程序。
如果有理解錯誤的地方,歡迎來信指出或發表評論。
附:垃圾郵件實在討厭,請高抬貴手。
原文章在 http://www.codeproject.com/aspnet/formsroleauth.ASP
原作者 Heath Stewart
概要:
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放在它的根目錄下,也就是通過 http://localhost/RolebasedAuth/Login.ASPx 可以訪問。
這個Lo
gin.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 UserName=@username " +
"AND Password=@passWord";
// 填充各個參數
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();
&nb
sp; 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="http://schemas.microsoft.com/intellisense/IE5 ">
</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:Bu
tton></P>
</form>
</body>
</Html>
你會注意到上面我們對密碼的處理:將它哈希加密。哈希加密是一種單向算法(不可逆算法),生成唯一的字符數組。因此即使是改變密碼中一個字母的大小寫,都會生成完全不同的哈希列。我們把這些加密的密碼存儲在數據庫中,這樣更安全。在實際應用中,你可能想為用戶找回忘記的密碼。但是哈希散列是不可逆的,所以你就不可能恢復原來的密碼。但是你可以更改用戶的密碼,並且把這個更改後的密碼告訴他。如果一個網站能夠給你舊密碼,那麼你要考慮清楚了,你的用戶數據是不安全的!事實上,國內大部分網站都是沒有經過加密直接把用戶的密碼存儲到數據庫中的。如何一個黑客入侵成功,那麼這些用戶帳戶就很危險了!
如果沒有使用SSL,你的密碼在網絡中也是以明文傳輸的。傳輸過程中可能會被竊取。在服務器端加密密碼只能保證密碼存儲的安全。SSL相關的資料可以在 http://www.versign.com 或 http://www.thewte.com 中找到。
如果你不想以加密方式在數據庫中存儲密碼,你可以更改上面的代碼,把
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="*" />
至此,我們已經為網站配置了基於角色的安全認證機制了。你可以先編譯你的程序,然後嘗試訪問一個機密目錄,例如 http://localhost/RolebasedAuth/Admin ,這時候你就會被轉向到用戶登錄頁面。如果你登錄成功,並且你的角色對這個目錄有訪問權限,你就重新回到這個目錄下。可能會有用戶(或入侵者)企圖進入機密目錄,我們可以使用一個 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>歡迎來到阿木小屋 http://amuhouse.com/ ^_^</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 實現了基於角色的安全機制。它並不是一個很難實現的機制,不過它可能需要一些相關知識如 什麼是用戶憑證,如何認證用戶身份,以及如何審定授權用戶。如果你覺得它很有幫助,我會非常高興。我希望它可以引導你在你的網站中去實現基於角色的窗體安全認證機制。
附:
本文的示例項目源代碼:
http://www.amuhouse.com/blog/upload/RolebasedAuth.rar