|
最近,要做一個登陸的頁面,就想到了安全性方面的問題。記得曾經(jīng)在邵志東老師講的關(guān)于asp.net安全性方面的課程中,提到asp.net提供了4個身份驗證程序:1.表單身份驗證;2.Windows身份驗證;3.Passport身份驗證;4.默認身份驗證。尤其講了表單身份驗證,想想,正好自己以前也不曾使用過這個驗證方式,那就拿來練練手吧。 表單驗證,可以根據(jù)用戶和角色來限制用戶訪問。比如,我們有以一個后臺管理系統(tǒng),只有通過后臺登陸頁面合法登陸的用戶才能訪問后臺管理系統(tǒng)中的任何頁面,這個時候我們就可以通過表單驗證來實現(xiàn)(過去我都是在每一個頁面寫判斷邏輯。現(xiàn)在想起來,過去的那種方法真是不折不扣的體力勞動,而且如果哪個頁面忘記寫了,就麻煩了)。 實驗開始(因為只記錄經(jīng)驗,所以有些知識點這里并沒有提到,需要大家多花點課外時間了。文末提供了些鏈接供大家參考) 我接下來就來做一個Forms表單驗證的例子。在該例子中,我建立了兩個文件夾分別為User和Admin,在每一個文件夾中又有l(wèi)ogin.aspx、index.aspx和web.config。我希望普通用戶訪問User文件夾需要首先要在用戶登陸界面進行登陸,成功后才能訪問用戶的index.aspx。而管理員則首先要在Admin的登陸界面進行登陸,才能訪問Admin中的index.aspx。而在網(wǎng)站根目錄有LoginRedirect.aspx、web.config和Global.asax。 如何才能實現(xiàn)表單驗證呢? <authentication mode="Forms"> <forms name="adminlogin" loginUrl="loginRedirect.aspx"> </forms> </authentication> <authorization> <allow users="*"/> </authorization> 上述的配置是什么意思呢? 還有其它諸多元素,請大家自己查找相關(guān)資料。我也會在文末給出一些我認為比較不錯的鏈接。 講完了“驗證”節(jié),接著講講“授權(quán)”節(jié)。 再來看看兩個子文件夾內(nèi)的web.config。 <configuration> <location path="login.aspx"> <system.web> <authorization> <allow users="*"/> </authorization> </system.web> </location> <system.web> <authorization> <allow roles="user"/> <deny users="*"/> </authorization> </system.web> </configuration>
<configuration> <location path="login.aspx"> <system.web> <authorization> <allow users="*"/> </authorization> </system.web> </location> <system.web> <authorization> <allow roles="Manager"/> <deny users="*"/> </authorization> </system.web> </configuration> 配置好了之后,我們還需要寫一些cs代碼。首先我們來看一下loginRedirect.aspx.cs。因為,我們現(xiàn)在訪問上述任何一個子文件的頁面時,都會先跳轉(zhuǎn)到這個頁面里來,而其實我則希望可以跳到相信子目錄的登陸頁面中去。因此,需要在這個文件中進行一些判斷。 string from = Request.QueryString["ReturnUrl"];//每個跳轉(zhuǎn)過來的頁面都會帶有ReturnUrl值,以此來獲取跳轉(zhuǎn)之前的頁面 //獲取子目錄名稱 string fromFilePath = from.Substring(from.IndexOf('/') + 1, from.IndexOf('/', from.IndexOf('/') + 1) - from.IndexOf('/')-1); //根據(jù)子目錄名稱來判斷跳轉(zhuǎn)的鏈接 switch (fromFilePath.ToLower()) { case "admin": Response.Redirect("/admin/login.aspx"); break; case "user": Response.Redirect("/user/login.aspx"); break; } 有些人可能奇怪了,這么麻煩,既然可以在“驗證”節(jié)中配置loginUrl,難道就不能對每個目錄實現(xiàn)直接跳轉(zhuǎn)到本目錄相應登陸頁面嗎?很遺憾,目前為止,我還沒有找到直接的解決辦法。如果您有什么辦法,請不吝賜教。 protected void Page_Load(object sender, EventArgs e) { //判斷用戶是否已經(jīng)登陸,且角色為user if (User.Identity.IsAuthenticated&&User.IsInRole("user")) {//如果通過驗證,則直接跳轉(zhuǎn)到index.aspx Response.Redirect("index.aspx"); } } //登陸按鈕事件,這里簡單起見,我直接以用戶名"user",密碼"1"來判斷,當然你也可以從數(shù)據(jù)庫讀取。 protected void btnLogin_Click(object sender, EventArgs e) { if (tbUserName.Text == "user" && tbPwd.Text == "1") { //生成驗證票據(jù),其中包括用戶名、生效時間、過期時間、是否永久保存和用戶數(shù)據(jù)等。而關(guān)于用戶角色的信息,我們保存在用戶數(shù)據(jù)中。 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, tbUserName.Text, DateTime.Now, DateTime.Now.AddMinutes(30), true, "User"); string cookieStr = FormsAuthentication.Encrypt(ticket);//對票據(jù)進行加密 HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieStr); /*保存到cookie中。cookie的名字要與我們前面在配置文件中所寫的name值一樣。因為,當cookie保留在本地后,下次再檢查用戶權(quán)限的時候就會自動查找與forms名稱相同的cookie,并傳送給服務器端進行檢驗。如果在本地找不到cookie,就自然無法通過驗證。*/ cookie.Expires = ticket.Expiration; cookie.Path = FormsAuthentication.FormsCookiePath; Response.Cookies.Add(cookie); Response.Redirect("index.aspx");//登陸成功后跳轉(zhuǎn)到index.aspx } } /* 這里突然冒出一個票據(jù),有些朋友是不是很奇怪呀?票據(jù)什么用呢? 票據(jù)其實也可以理解為憑據(jù)(只有有憑據(jù)的用戶才能通過檢查),它包括了上面注釋中所寫的一些與用戶相關(guān)的信息。但是票據(jù)不能直接傳送給服務器必須通過cookie來承載。而服務器端在接受到cookie之后,會從中取出票據(jù)的數(shù)據(jù),并進行相關(guān)操作。 */ 在Admin文件夾下的login.aspx.cs也是類似。就不再贅述了。 protected void Application_AuthenticateRequest(object sender, EventArgs e) { if (HttpContext.Current.User != null) {//如果用戶通過驗證,則該項不為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;//取出角色數(shù)據(jù) string[] roles = userData.Split(','); HttpContext.Current.User = new GenericPrincipal(id, roles);//重新分配角色 } } } } 大家可以下載整個工程來看。
public void Init(HttpApplication app) { app.AuthenticateRequest += new EventHandler(this.OnEnter); app.EndRequest += new EventHandler(this.OnLeave); } Init方法注冊了兩個事件:OnEnter和OnLeave。分別在HttpApplication.AuthenticateRequest和EndRequst事件被觸發(fā)時執(zhí)行。 這兩個事件具體做了些什么呢? private void OnEnter(object source, EventArgs eventArgs) { if (!_fAuthChecked || _fAuthRequired) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; Trace("*******************Request path: " + context.Request.PathWithQueryString); //從Web.Config中獲取authentication配置信息 AuthenticationSection authentication = RuntimeConfig.GetAppConfig().Authentication; authentication.ValidateAuthenticationMode(); if (!_fAuthChecked) { //設置是否為Forms驗證,如果是_fAuthRequired 設為true _fAuthRequired = authentication.Mode == AuthenticationMode.Forms; _fAuthChecked = true; } if (_fAuthRequired) { if (!this._fFormsInit) { Trace("Initializing Forms Auth Manager"); //初始化驗證信息,Initialize方法主要是通過讀取配置文件的authentication節(jié)來初始化FormsName、LoginUrl等 FormsAuthentication.Initialize(); this._FormsName = authentication.Forms.Name; if (this._FormsName == null) { this._FormsName = ".ASPXAUTH"; } Trace("Forms name is: " + this._FormsName); this._LoginUrl = authentication.Forms.LoginUrl; if (this._LoginUrl == null) { this._LoginUrl = "login.aspx"; } this._fFormsInit = true; } //以下方法用于設置通過驗證的用戶標識[重要] this.OnAuthenticate(new FormsAuthenticationEventArgs(context)); CookielessHelperClass cookielessHelper = context.CookielessHelper; //下面的語句,應該是為了修改_skipAuthorization.該值指定 UrlAuthorizationModule 對象是否應跳過對當前請求的授權(quán)檢查。 //Forms 身份驗證模塊和 Passport 身份驗證模塊在重定向到已配置的登錄頁時都設置 SkipAuthorization。[MSDN] //如果為false則要進行授權(quán)檢查,否則就跳過檢查。[我的體會:如果不在代碼上進行控制,一般該值都為false。] if (AuthenticationConfig.AccessingLoginPage(context, this._LoginUrl)) { context._skipAuthorization = true; cookielessHelper.RedirectWithDetectionIfRequired(null, FormsAuthentication.CookieMode); } if (!context.SkipAuthorization) { context._skipAuthorization = AssemblyResourceLoader.IsValidWebResourceRequest(context); } } } } 在OnEnter事件中,我們提到一個重要的方法就是OnAuthenticate: //通過這個方法,我們就可以得到一個通過驗證User標識 private void OnAuthenticate(FormsAuthenticationEventArgs e) { HttpCookie cookie = null; //_eventHandler是一個FormsAuthenticationModule類的Authenticate事件??梢酝ㄟ^在asp.net應用程序的Global.asax文件中進行處理 if (this._eventHandler != null) { this._eventHandler(this, e); } //判斷用戶是否已經(jīng)通過驗證,如果已經(jīng)通過驗證則方法結(jié)束。通過驗證的用戶,其User標識不為Null。 if ((e.Context.User != null) || (e.User != null)) { if (e.Context.User == null) { e.Context._user = e.User; } } else { FormsAuthenticationTicket tOld = null; bool cookielessTicket = false; try { //從Cookie數(shù)據(jù)中提取驗證票據(jù)的數(shù)據(jù) tOld = ExtractTicketFromCookie(e.Context, this._FormsName, out cookielessTicket); } catch { tOld = null; } if ((tOld != null) && !tOld.Expired) { FormsAuthenticationTicket ticket = tOld; if (FormsAuthentication.SlidingExpiration) { //更新驗證票據(jù),根據(jù)所設置的過期時間來判斷 ticket = FormsAuthentication.RenewTicketIfOld(tOld); } //根據(jù)票據(jù)信息來創(chuàng)建用戶標識。第二個參數(shù)是用于對用戶授于某種角色用的,但是從new string[0]可以看出此處不含角色數(shù)據(jù)。 //如果我們需要對用戶的角色進行配置,可以在FormsAuthenticationModule類的Authenticate事件中配置 e.Context._user = new GenericPrincipal(new FormsIdentity(ticket), new string[0]); if (!cookielessTicket && !ticket.CookiePath.Equals("/")) { cookie = e.Context.Request.Cookies[this._FormsName]; if (cookie != null) { cookie.Path = ticket.CookiePath; } } //如果票據(jù)是新的,則生成一個新的Cookie給客戶端 if (ticket != tOld) { if ((cookielessTicket && (ticket.CookiePath != "/")) && (ticket.CookiePath.Length > 1)) { ticket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, ticket.UserData, "/"); } string cookieValue = FormsAuthentication.Encrypt(ticket); if (cookielessTicket) { e.Context.CookielessHelper.SetCookieValue('F', cookieValue); e.Context.Response.Redirect(e.Context.Request.PathWithQueryString); } else { if (cookie != null) { cookie = e.Context.Request.Cookies[this._FormsName]; } if (cookie == null) { cookie = new HttpCookie(this._FormsName, cookieValue); cookie.Path = ticket.CookiePath; } if (ticket.IsPersistent) { cookie.Expires = ticket.Expiration; } cookie.Value = cookieValue; cookie.Secure = FormsAuthentication.RequireSSL; cookie.HttpOnly = true; if (FormsAuthentication.CookieDomain != null) { cookie.Domain = FormsAuthentication.CookieDomain; } e.Context.Response.Cookies.Add(cookie); } } } } } 在執(zhí)行了這個module之后,還有一個重要的module我們不得不提的就是UrlAuthorizationModule。在這個module中,對上面所設置的用戶進行了授權(quán)檢查,來確定該用戶是否可以訪問所請求的頁面。如果用戶沒有權(quán)限,則跳轉(zhuǎn)到loginUrl中所指定的頁面。主要方法就是OnEnter: private void OnEnter(object source, EventArgs eventArgs) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; if (context.SkipAuthorization) { if (!context.User.Identity.IsAuthenticated) { PerfCounters.IncrementCounter(AppPerfCounter.ANONYMOUS_REQUESTS); } } else { //讀取web.config中配置的授權(quán)信息 AuthorizationSection authorization = RuntimeConfig.GetConfig(context).Authorization; //IsUserAllowed便是對用戶進行授權(quán)檢查 if (!authorization.EveryoneAllowed && !authorization.IsUserAllowed(context.User, context.Request.RequestType)) { context.Response.StatusCode = 0x191;//用戶沒有被授權(quán),[記住這個標識] this.WriteErrorMessage(context); if (context.User.Identity.IsAuthenticated) { WebBaseEvent.RaiseSystemEvent(this, 0xfa7); } application.CompleteRequest(); } else { if (!context.User.Identity.IsAuthenticated) { PerfCounters.IncrementCounter(AppPerfCounter.ANONYMOUS_REQUESTS); } WebBaseEvent.RaiseSystemEvent(this, 0xfa3); } } } private void OnLeave(object source, EventArgs eventArgs) { if (_fAuthChecked && _fAuthRequired) { HttpApplication application = (HttpApplication) source; HttpContext context = application.Context; //如果標識為0x191,則跳轉(zhuǎn)到loginUrl if (context.Response.StatusCode == 0x191) { string str3; string strUrl = null; if (!string.IsNullOrEmpty(this._LoginUrl)) { strUrl = AuthenticationConfig.GetCompleteLoginUrl(context, this._LoginUrl); } if ((strUrl == null) || (strUrl.Length <= 0)) { throw new HttpException(SR.GetString("Auth_Invalid_Login_Url")); } CookielessHelperClass cookielessHelper = context.CookielessHelper; string pathWithQueryString = context.Request.PathWithQueryString; if (strUrl.IndexOf('?') >= 0) { str3 = FormsAuthentication.RemoveQueryStringVariableFromUrl(strUrl, "ReturnUrl") + "&ReturnUrl=" + HttpUtility.UrlEncode(pathWithQueryString, context.Request.ContentEncoding); } else { str3 = strUrl + "?ReturnUrl=" + HttpUtility.UrlEncode(pathWithQueryString, context.Request.ContentEncoding); } int index = pathWithQueryString.IndexOf('?'); if ((index >= 0) && (index < (pathWithQueryString.Length - 1))) { pathWithQueryString = FormsAuthentication.RemoveQueryStringVariableFromUrl(pathWithQueryString, "ReturnUrl"); } index = pathWithQueryString.IndexOf('?'); if ((index >= 0) && (index < (pathWithQueryString.Length - 1))) { str3 = str3 + "&" + pathWithQueryString.Substring(index + 1); } cookielessHelper.SetCookieValue('F', null); cookielessHelper.RedirectWithDetectionIfRequired(str3, FormsAuthentication.CookieMode); context.Response.Redirect(str3, false); } } } 根據(jù)這個流程,結(jié)合前面的實驗,我們來模擬以下這個執(zhí)行過程。 上圖為一次Http請求
上圖為一次Http應答
流程介紹結(jié)束 寫的不當之處,希望大家直言不諱~~ 整個工程下載:http://files.cnblogs.com/stg609/LoginAuthentication.rar(開發(fā)平臺:VS 2008. 不過,基本沒用到.net 3.0的東西,可能可以順利轉(zhuǎn)換到VS 2005上使用) 推薦閱讀:http://www.cnblogs.com/cuihongyu3503319/archive/2008/09/11/1288956.html |
|
|