ASP.NET 表单验证实现浅析

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);  

posted @ 2009-08-06 13:56  三颗屎  阅读(5738)  评论(0编辑  收藏  举报