ASP.NET 表單驗證實現淺析

來源:互聯網
上載者:User
ASP.NET 表單驗證實現淺析

對於Web應用的表單身分識別驗證,因為公司有一個類庫,採用 Session 實現,所以一直都沒有去仔細瞭解。其實我並不贊成在 .NET 中用 Session 實現身分識別驗證,畢竟 .NET 提供了一個強大的身分識別驗證體系,並且公司的類庫也沒有實現什麼特殊的功能,僅只是儲存一個 Session 變數來提供身份識別,在安全性和可用性上與 .NET 的實現相比,個人感覺還是有較大的差距。

近期很少加班,就抽空看了一下,理了個大致的思路出來。

首先,自然是配置 Web.config,在 <system.web> 下設定:

<authentication mode="Forms">

<forms name=".SomeTsteAuth"

loginUrl="admin/login.aspx"

defaultUrl="admin/index.aspx"

path="/"

timeout="10">

</forms>

</authentication>

<authentication> 的 Mode=”Forms” 指定 Web 應用程式採用表單驗證,另外的方式還有“Windows”、“Passport”和“None”,“Windows”常用在區域網路中,配合 AD 進行身分識別驗證,“Passport”好像要交錢給微軟後才能夠使用,不太清楚了。“None”表示不進行驗證。

<forms> 的幾個常用屬性:

name 屬性指定驗證所需要的 cookie 的名稱,預設值是“.ASPXAUTH”,如果在一個伺服器上下掛了多個 Web 應用程式,必須重新指定該名稱,因為每個應用程式都需要唯一的 cookie。

loginUrl 屬性指定登入用的頁面,用於提供使用者名稱和密碼,預設值是“login.aspx”。該頁面可以和需要提供身分識別驗證才能訪問的頁面放在同一個目錄下(呵,我原以為這個頁面要放在單獨的可公開訪問的目錄下)。

defaultUrl 屬性指定登入後跳轉到的頁面,預設值是“default.aspx”,當然你也可以跳轉到使用者登入前的前一個頁面,並且這是 .NET 的預設實現。

path 屬性指定 cookie 的路徑,預設值為“/”,對於大多數瀏覽器而言,cookie 的 path 是區分大小寫,因此如果路徑的大小寫不符,將不會發回 cookie。(注意:“/”指網站的根目錄,在開發時,Visual studio 通常會在網站根目錄下建立一個新的目錄作為 Web 應用程式的根,例如http://localhost/MySite,此時若你要單獨針對 Web 應用程式設定 path,必須從網站根目錄後的路徑指定,例如,要設定剛才的 Web 應用程式訪問 Admin 目錄下的檔案需要身分識別驗證時,path 值應為“MySite/Admin”,而不是“Admin”,否則使用者將無法正常登入。)

timeout 屬性指定使用者多長時間不進行操作,身份憑證會到期,以分鐘為單位,預設為 30 分鐘。

設定好 <authentication> 以後,還需要設定 <authorication>,最常見的方式如下:

                <authorization>            

            <deny users="?"/>  

        </authorization>

“?”號表示匿名使用者,“*”號表示所有使用者。在網上看到一些文檔,在<deny users=”?” /> 下面還有一句 <allow users=”*” />,其實沒必要增加這一句,除非使用角色對各個子目錄分別進行許可權控制。因為這樣有一個潛在的危險,如果有人不小心把 <allow users=”*” /> 放在了 <deny user=”?” /> 前面,系統將不會進行驗證。

如果要對子目錄進行分別的許可權管控,需要新增一個 <location> 段到 Web.config 的根項目 <configuration> 下:

<location path="admin">

     <system.web>

         <authorization>

             <deny users="?"/>

         </authorization>

     </system.web>

</location>

如果涉及到多個子目錄,並要分配不同的許可權,那就需要使用角色。將 <authorization> 下的內容換成

<allow roles="Admin"/>

<deny users="*"/>

Web.config 的配置大致如此,需要注意的一點是 <authentication> 節一個 Web 應用程式只能有一個,即我們在一個 Web 應用程式中只能採用一種驗證方式,而 <location> 節可以配置多個,以對各目錄進行不同的許可權管控。對於未配置 <authorization> 節的目錄,存取權限與 Web 應用程式根相同,若應用程式的根也未配置 <authorization>,則預設為任何人可訪問,即使 <authentication> 的 mode 屬性設定為“Forms”。

       

其次就是編碼,在登入頁面的“登入”按鈕點擊事件中:

        if (UserName.Text.Trim() == "你的使用者名稱" && Password.Text == "你的密碼")

            FormsAuthentication.RedirectFromLoginPage("你的使用者名稱", false);

    else

        //提示使用者名稱和密碼不正確

RedirectFromLoginPage() 的第一個參數是目前正在驗證的使用者名稱,第二個參數指是否長期儲存登入資訊到 cookie 中,這個參數的意義在 .NET 2.0 中與 .NET 1.1 中不同,在 .NET 1.1 中,會將登入資訊儲存到 cookie 中,並設定到期時間為 50 年後,即你以後再也不用輸入使用者名稱和密碼,除非 cookie 被刪除或 50 年後(老眼昏花的你加上一台堪稱古董的電腦,最美不過夕陽紅)。在 .NET 2.0 中,這個參數僅指在關閉瀏覽器後,登入資訊在 cookie 中是否還存在,而到期時間的約束依然有效,即 cookie 到期後,無論你重啟瀏覽器與否,仍需輸入登入憑證。

代碼簡單得出乎意料,.NET 會自動建立票劵並重新導向到登入前使用者訪問的那個需要進行驗證的頁面,若使用者直接存取登入頁面,則重新導向到 Web.config 中定義的 default 頁面。如果需要自己控制重新導向的過程,可以這麼做:

        if (UserName.Text.Trim() == "你的使用者名稱" && Password.Text == "你的密碼")

    {

        FormsAuthentication.SetAuthCookie("你的使用者名稱", false);

        Response.Redirect("Index.aspx");

    }

    else

        //提示使用者名稱和密碼不正確

其實,語句 FormsAuthentication.RedirectFromLoginPage("你的使用者名稱", false) 等同於

FormsAuthentication.SetAuthCookie("你的使用者名稱", false);

Response.Redirect(FormsAuthentication.GetRedirectUrl("你的使用者名稱", false));

當然,也可以手動建立票劵,並加入到響應的 cookie 集合中,完整的代碼如下:

    if (UserName.Text.Trim() == "你的使用者名稱" && Password.Text == "你的密碼")

    {

            //為當前登入使用者建立一個新的票劵

        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(

2, //版本號碼

"你的使用者名稱", //登入的使用者名稱

DateTime.Now, //票劵發布時間

DateTime.Now.AddMinutes(15), //票劵到期時間

false, //是否在關閉瀏覽器後仍然保留登入資訊

"", //可加入少量使用者資料(注意:不能為 null)

FormsAuthentication.FormsCookiePath //cookie 路徑

);

        //加密票劵,擷取加密後的字串

        string encrypt = FormsAuthentication.Encrypt(ticket);

        //使用加密後的字串建立一個 cookie

        HttpCookie cookie = new HttpCookie(

FormsAuthentication.FormsCookieName,

Encrypt

);     

        //將 cookie 增加到用戶端

        Response.Cookies.Add(cookie);

        Response.Redirect(FormsAuthentication.GetRedirectUrl("你的使用者名稱", false));

    }

    else

        //提示使用者名稱和密碼不正確

 

到這裡,應該很清楚了,表單驗證,其實質是使用一個特定的 cookie,在每次串連伺服器時驗證該 cookie 是否存在,從而決定使用者是否具有相應的許可權。在上述代碼中,也可以增加對 cookie 控制的代碼,在使用加密後的票劵建立一個 cookie 後,增加代碼:

    //HttpOnly 屬性為 true,表示該 cookie 不能在瀏覽器端進行存取

    cookie.HttpOnly = true;

    //cookie 的路徑,取 Web.config 中 <forms> 屬性 path 的值

    cookie.Path = FormsAuthentication.FormsCookiePath;

    //設定 cookie 的到期時間,與票劵的到期時間一致,如果這兩個時間不一致,則其中任何一個時間到期時,均視為到期           

    cookie.Expires = ticket.Expiration;

 

清楚表單驗證的大致機制後,對於基於角色的表單驗證也是手到擒來,大致的過程敘述如下:

1 在登入頁面的代碼檔案中,新增一個普通的 cookie,將使用者所屬的角色儲存到該 cookie 中(一個使用者可具備多個角色)。

2 在 Global.asax 的 AuthenticateRequest 事件中,判斷使用者是否已通過驗證,若已通過驗證,則從1增加的 cookie 中取出角色字串,並構建一個 System.Security.Principal.GenericPrincipal 對象,該對象的建構函式包括兩個參數:使用者標識和角色數組,使用者標識可通過 HttpContext.Current.User.Identity 取得,角色數組將角色字串轉換為字串數組賦進去即可。

3 調整 Web.config 設定角色的許可權。

  

    這兩天在ERC的項目中重新設計使用者驗證,一開始已經實現了簡單的利用ASP.NET的FORM驗證實現了對使用者登陸和安全檢查的監測,但是由於牽涉到一個使用者權限的分配和基於使用者權限的UI顯示,所以想對原有利用Session的讀取方法加以改進,主要是考慮到Session的不穩定性。
    首先想到的就是利用附加到Page.User的GenericPrincipal對象當中去,根據MSDN的協助資訊顯示,可以看出GenericPrincipal對象是Microsoft設計的用於結合FORM驗證對Active域或者IIS使用者的許可權讀取的一個普通使用者物件,當然我們可以手動將我們的資料附加到改對象中,並將改對象捆綁到Page.User對象中。經過漫長的調試,在昨天也就是周五基本實現了這個思路,具體代碼如下
    //構造人工身份證票據字串
    string m_UserData = Username.ToString() + "," + strRole.ToString() + "," + LastLoginIp.ToString() + "," + LastLoginTime.ToString();
    //建立身份證票據對象,對象名為"Ticket",內建名"ERCUser",到期時間20分鐘,包含使用者名稱、使用者權限、使用者最後登陸IP,使用者最後登陸時間資訊
    FormsAuthenticationTicket Ticket = new FormsAuthenticationTicket(
1,"ERCUser",DateTime.Now,DateTime.Now.AddMinutes(20),false,m_UserData,"/");
    //加密序列化驗證票為字串
    string EncryptTicket = FormsAuthentication.Encrypt(Ticket);
    //產生
    //Http ERC = new Http(FormsAuthentication.FormsName,EncryptTicket);
    Http ERC = new Http("WebbUser",EncryptTicket);
    //將添加到Context上下文中
    HttpContext.Current.Response.s.Add(ERC);
    Response.s["WebbUser"].Expires = DateTime.Now.AddMinutes(20);
    由於我是在CallBack機制中實現的,所以沒有調用頁面回傳,因此下面這條語句在不是無重新整理機制的情況下可能需要調用
    //Context.Response.Redirect(Context.Request["ReturnUrl"]);
  
    上述代碼我們構建了一個通過由ASP.NET的FormAuthenticaion加密的對象,在調試過程中,原本是使用FormsAuthentication.FormsName來作為這個的名字來儲存(不知道FormsAuthentication.FormSName是什嗎?去翻MSDN,這個是要打PP的),但是經過N遍的調試,我才發現導致我在Global.asax中讀取資訊的就是因為不該使用了系統預設的名字,這個教訓非常慘痛,經過N遍的調試,我才將錯誤的原因定位在此,後來在跟蹤調試的時候發現,只要我的WEB頁面一重新整理,系統預設名字的資訊就會改變,為什麼呢?我還不知道呢。。。後面我修改使用WebbUser來作名字,就可以正常讀取加密的資訊了。
    之後我們就可以在Global.asax的Application_AuthenticateRequest的事件中來處理了,實現代碼如下:

    //在Global.asax中將該資訊添加到服務端表示使用者身份的GenericPrincipal對象中

    Http ERC = Ctx.Request.s[FormsAuthentication.FormsName];
    Http ERC = Ctx.Request.s["WebbUser"];

    FormsIdentity Id = (FormsIdentity)Ctx.User.Identity ;
    FormsAuthenticationTicket Ticket = null; //取得身分識別驗證票
    try
    {
        Ticket = FormsAuthentication.Decrypt(ERC.Value);
    }
    catch(Exception ex)
    {
        // Log exception details (omitted for simplicity)
        return;
    }

    if (null == Ticket)
    {
        // failed to decrypt.
        return;
    }

    string[] Roles = Ticket.UserData.Split (',') ; //將身分識別驗證票中的role資料轉成字串數組
    Ctx.User = new GenericPrincipal (Id, Roles) ; //將原有的Identity加上角色資訊建立一個GenericPrincipal表示目前使用者,這樣目前使用者就擁有了role資訊

    這樣,前面你構造的m_UserData字串也就被添加到了Page.User的GennericPrincial對象當中了,雖然成功的添加了該字串,最後在跟蹤調試的時候發現User.Identity的GennericPrincial對象中確實有了一個Collecation的對象,裡麵包含了m_UserData的資料,但是我卻無法將它們讀取出來,因為在MSDN對GennericPrincial對象的解釋中,只有一個InRole()的方法,只提供對所有使用者角色的(該角色表示Active域或者Windows使用者角色)檢測,後面本來想利用事件反射機制來讀取,不過最終以失敗告終,因為我不知道應該使用那個方法來反射。。。。真是失敗啊。。其代碼如下:
    IIdentity identity = User.Identity;
    MethodInfo method = identity.GetType().GetMethod("IsInRole",BindingFlags.Instance | BindingFlags.NonPublic);
    string[] roleNames =(string[])method.Invoke(identity,new object[]{});
    bool dd = User.IsInRole("test");
    系統編譯出錯,具體錯誤資訊不記得了....-_-
    通過一天一夜的折騰,最終改進FORM驗證的思路夭折了,痛哭~~不過通過調試,更加進一步瞭解了FORM驗證的機制和頁面載入處理的順序:(這裡不能畫圖,有機會再畫個貼上得了)

    既然附件許可權資料到User.Identity中失敗,只能採用另外的方法了,當然,也有人可能提出,可以自己寫IPrincial介面來重載對User對象的控制,那樣我覺得更複雜了,對偶的工作量還是沒有減輕,既然又懶得寫介面(應該是不會寫),只能寫PAGE基類來控制Session了,寫一個BasePage.cs的基類,從System.Web.Ui.Page繼承,重載Init()事件,在事件中檢測Session是否存在,不存在則重新從資料庫中讀取並重設,這個過程當然得控制在使用者通過票據驗證,以下是具體代碼;
    #region   事件處理  
    protected   override   void   OnInit(EventArgs   e)  
    {  
        base.OnInit(e);  
        //作登陸後Session檢測,防止Session穩定性失敗導致應用程式崩潰
        if(User.Identity.IsAuthenticated)
        {
            if(Session["Role"] == null || Session["UserName"] == null)
            {
                Member member = new Member();
       SqlDataReader sdr_member = member.GetUserInfo(User.Identity.Name.ToString());
       while(sdr_member.Read())
       {
   Session["Role"] = sdr_member["Role"].ToString();
   Session["UserName"] = sdr_member["Username"].ToString();
        }
   }
}

        this.Error   +=   new   System.EventHandler(this.PageBase_Error);  

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.