作者: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頁面。當然你也可以在你現有的網路應用程式專案中加入這些。
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可以存取。
這個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 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();
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:Button></P>
</form>
</body>
</HTML>
你會注意到上面我們對密碼的處理:將它雜湊加密。哈希加密是一種單向演算法(不可逆演算法),產生唯一的字元數組。因此即使是改變密碼中一個字母的大小寫,都會產生完全不同的雜湊。我們把這些加密的密碼儲存在資料庫中,這樣比較安全。在實際應用程式中,你可能會想為使用者找回忘記的密碼。但是哈希散列是不可逆的,所以你就不可能恢復原來的密碼。但是你可以更改使用者的密碼,並且把這個更改後的密碼告訴他。如果一個網站能夠給你舊密碼,那麼你要考慮清楚了,你的使用者資料是不安全的!事實上,國內大部分的網站都是沒有經過加密直接把使用者的密碼儲存到資料庫中。如何一個駭客入侵成功,那麼這些使用者帳號就很危險了!
如果沒有使用SSL,你的密碼在網路中也是以明文傳送的。傳輸過程中可能會被竊取。在伺服器端加密密碼只能確保密碼儲存的安全。 SSL相關的資料可在http://www.versign.com或http://www.thewte.com找到。
如果你不想以加密方式在資料庫中儲存密碼,你可以更改上面的程式碼,把
FormsAuthentication.HashPasswordForStoringInConfigFile(PasswordTextBox.Text, "md5") 改為PasswordTextBox.Text 即可。
下一步,我們需要修改Global.asax 檔案。如果你的網路應用程式沒有這個文件,請右鍵點擊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 實作了基於角色的安全機制。它並不是一個很難實現的機制,不過它可能需要一些相關知識如什麼是使用者憑證,如何認證使用者身份,以及如何審定授權使用者。如果你覺得它很有幫助,我會非常高興。我希望它可以引導你在你的網站中實現基於角色的窗體安全認證機制。
附:
本文的範例專案原始碼: