欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Asp.Net实现FORM认证的一些使用技巧(必看篇)

程序员文章站 2023-12-09 23:09:33
最近因为项目代码重构需要重新整理用户登录和权限控制的部分,现有的代码大体是参照了.net的form认证,并结合了portal kits的登录控制,代码比较啰嗦,可维护性比较...

最近因为项目代码重构需要重新整理用户登录和权限控制的部分,现有的代码大体是参照了.net的form认证,并结合了portal kits的登录控制,代码比较啰嗦,可维护性比较差。于是有了以下的几个需求(大多数系统应该都会碰到):

1.用.net自带的form认证来实现安全登录

2.登录后需要记录登录用户的基本信息,方便所有页面调用

3.记录本机登录状态,短时间关闭窗口后不用重新登录

4.权限控制和代码的文件夹结构相呼应,即按角色允许访问不同的目录

5.权限控制有可能需要细化到每一个页面,即按角色允许访问不同的页面

6.以上的部分尽量自己少写代码,用自带的类库和机制实现

第一步:准备工作

先准备一个名为test的web项目,包含:

default.aspx,默认页,随便显示一些信息,

login.aspx,登录页,上面放两个文本框,用来输入用户名和密码,一个登录按钮,一个指向register.aspx的超链,

register.aspx,用户注册页,注册用户信息,随便放一点文本框,主要是模拟一下注册,不用真正实现,

web.config,配置页面。

注册页与登录页在同一目录的机妙后面会说。

第二步:web.config文件的修改

1、打开web.config文件,找到authentication节,将其改为如下:

<authentication mode="forms">
  <forms name=".aspxauth" loginurl="login.aspx" protection="all" path="/" timeout="20"/>
</authentication>
<authorization>
  <deny users="?"></deny>
</authorization>

配置节属性的具体意义和其他没有加入的属性网上到处都有查。这里注意一下的是authentication节和authorization节,两个单词很相似,但却不是同一个单词,每个节下面的内容也不能写到一起。

其中,authorization节中的“allow”表示允许的意思,“*”表示所有用户;而“deny”表示拒绝的意思;“?”表示匿名用户;此处加入后,则代表根目录下的所有文件和所有的子目录都不能匿名访问,login.aspx 页面除外。

2、web.config中,location节的应用

做了上面的配置之后,我们会发现,在没登录的情况下,用浏览器打开default.aspx会自动转到login.aspx,同理register.aspx页面也会如此。问题:注册用户怎么可能在登录后才能访问呢?

那么我们就得说了,当注册页与登录页在同一目录,为了达到不用登录就能访问注册页的目的,我们就得对访问限制的web.config配置处理一下。

方法一 :注册页与登录页放在不的目录内

我们在根目录添加一个文件夹pub,将register.aspx移动到此文件夹里,此时仍不能访问,需要在文件夹内添加一个web.config文件,加入:

<configuration> 
 <system.web> 
  <authorization>
   <allow users="*"/>
  </authorization> 
 </system.web> 
</configuration> 

此处,即说明此目录下的所有文件,允许所有人访问。

关于 web.config 作用范围的说明:

• web.config 的设置将作用于所在目录的所有文件及其子目录下的所有东东(继承:子随父姓)

• 子目录下的 web.config 设置将覆盖由父目录继承下来的设置(覆盖:县官不如现管)

• 也就是,属性设置由最深一层的目录里的web.config决定;如果子目录里没有web.config文件,则由离它最近的父目录里的web.config决定

方法二:仍然保持注册页和登录页在同一目录下

只需要在根目录下的web.config 中加入以下一段:

<location path="register.aspx">
 <system.web>
   <authorization>
    <allow users="*"/>
   </authorization>
 </system.web>
</location>

通过location节的path属性的值指定register.aspx页面,以及下面authorization节的设置,说明了register.aspx页面是允许被所有人访问。

注意:

location节应加在原有的system.web节的外面,包含在configuration节内,和system.web节是同级的。  

当根目录下,有多个页面不需要登录就可以访问时,可以设置多个location节,修改对应path属性值指向的页面就可以了。

另外,path属性的值也可以指定目录,用来指定该目录的访问限制。通过修改authorization节的内容来限定访问权限。详细的设置,后面会提到。

第三步:实现登录的代码

1、普通的代码实现

方法一:

如果forms节中设置了“defaulturl”的属性,也就是登录后默认转向的页面,则可以用如下的方法:

private void btn_login_click(object sender, system.eventargs e) 
{ 
 if(this.txt_username.text=="admin" && this.txt_password.text=="123456") 
 { 
   formsauthentication.redirectfromloginpage(this.txt_username.text,false); 
 } 
} 

此处只是简单模拟了一下登录的验证过程,redirectfromloginpage方法能发送验证票据验证cookie(如何进行可以用reflector去查看源代码),返回请求页面,即“从哪来就打哪去”。比如:用户没登录前直接在 ie 地址栏输入 http://localhost/test/default.aspx ,那么该用户将看到的是 login.aspx?returnurl=default.aspx ,输入用户名与密码登录成功后,系统将根据“returnurl”的值,返回相应的页面;如果没有“returnurl”,则按照“defaulturl”的属性自动转向。

方法二:

private void btn_login_click(object sender, system.eventargs e)
{ 
  if(this.txt_username.text=="admin" && this.txt_password.text=="123456") 
  { 
   formsauthentication.setauthcookie(this.txt_username.text,false); 
   response.redirect("default.aspx"); 
  } 
} 


此处是分两步走:通过验证后就直接发放 cookie ,跳转页面将由程序员自行指定,无需“defaulturl”设置。此方法对于程序员来说,更灵活。  

2、手工实现需要记录用户登录信息的情况

当我们需要记录用户登录的信息,不单单只是一个id还需要更多属性的时候,一般都用一个类存储到session或cookie实现,然后做一个基类页,在基类页中设置属性来读取session或cookie。

session实际也和支不支持cookie有关,且存在服务器,多少会占用服务器端资源。因此这里还是考虑用cookie实现。那么在redirectfromloginpage方法或setauthcookie方法已经设置了验证票据并设置了cookie,我们能不能把用户登录信息也存储到这个默认的cookie里呢?答案是能。

首先,我们在项目里添加appcode目录,增加一个userinfo的类,用以简单模拟用户登录信息。代码如下:

[serializable]
public class userinfo
{
  //用户登录信息
  private int _nid;
  private string _srealname;
  private string _sname;  
  private string _spassword;
  private string _sroles;

  public int id
  {
    get { return this._nid; }
    set { this._nid = value; }
  }
  public string realname
  {
    get { return this._srealname; }
    set { this._srealname = value; }
  }
  public string name
  {
    get { return this._sname; }
    set { this._sname = value; }
  }
  public string password
  {
    get { return this._spassword; }
    set { this._spassword = value; }
  }
  public string roles
  {
    get { return this._sroles; }
    set { this._sroles = value; }
  }

  public userinfo()
  {    
  }
}

需要注意, 类的属性中一定要加[serializable],表示类可以序列化。

forms验证在内部的机制是,把用户数据加密后保存在一个基于cookie的票据formsauthenticationticket中,通过redirectfromloginpage方法或setauthcookie方法就已经实现了ticket和cookie的设置,也就是设置了context.user的值,context.user在取值和判断是否经过验证的时候很有用处。cookie的属性是在web.config的<forms name=".aspxauth" loginurl="login.aspx" protection="all" path="/" timeout="20"/>中设置的。因为是经过特殊加密的,所以应该来说是比较安全的。

而.net除了用这个票据存放自己的信息外,还留了一个地给用户*支配,这就是现在要说的ticket的userdata。 userdata用来存储string类型的信息,并且也享受forms验证提供的加密保护,当我们需要这些信息时,也可以通过简单的ticket的 userdata属性得到,兼顾了安全性和易用性,用来保存一些必须的敏感信息还是很有用的。我们就准备将用户的登录信息记录在userdata中,代码如下:

protected void button1_click(object sender, eventargs e)
  {
    if (this.textbox1.text == "admin" && this.textbox2.text == "123456")
    {
      // 加密userinfo
      userinfo user = new userinfo();
      user.id = 1;
      user.name = this.textbox1.text;
      user.password = this.textbox2.text;
      user.realname = "系统管理员";
      user.roles = "administrators,users";
      string struser = serialize.encrypt<userinfo>(user);

      // 设置ticket信息
      formsauthenticationticket ticket = new formsauthenticationticket(
        1, user.name, datetime.now, datetime.now.addminutes(20), false, struser);

      // 加密验证票据
      string strticket = formsauthentication.encrypt(ticket);

      // 使用新userdata保存cookie
      httpcookie cookie = new httpcookie(formsauthentication.formscookiename, strticket);
      cookie.expires = ticket.expiration;
      this.response.cookies.add(cookie);
      

      this.response.redirect("default.aspx");
    }
}

上面的代码,实际上类似于手工实现了setauthcookie方法的过程。

首先,模拟实现登录,我们手动设置了一个userinfo的对象,string struser = serialize.encrypt<userinfo>(user) 是将对象序列化成字符串的一个方法。

  然后,生成一个formsauthenticationticket票据。此处用到的formsauthenticationticket构造函数的重载方法的签名解释

public formsauthenticationticket( 
    int version, //版本号
    string name, //与身份验证票关联的用户名
    datetime issuedate, //票据的发出时间
    datetime expiration,//票据的到期日期
    bool ispersistent, //票据是否存储在持久的 cookie 中,是为 true;否则为 false
    string userdata //票据中存储的用户定义数据
);

再后,string strticket = formsauthentication.encrypt(ticket) 将票据加密成字符创

最后,httpcookie cookie = new httpcookie(formsauthentication.formscookiename, strticket) 生成cookie。

formsauthentication.formscookiename获取的就是web.config中配置的cookie名称,也就是默认验证时产生的cookie。cookie.expires = ticket.expiration 将票据的过期时间和cookie的过期时间做了同步,也就避免了两者不同所产生的矛盾。这样,验证票据生成了,存储到默认配置的cookie中,也就是类似实现了一个setauthcookie方法的过程。通过context.user就能获取票据的相关信息了。

3、获取信息

为了在其他登录后的页面比较简单的获取登录用户信息,我们先生成一个基类页面。在appcode中新增loginbasepage类,代码如下:

public class loginbasepage : page
{
  protected userinfo loginuser
  {
    get
    {
      string struser = ((formsidentity)this.context.user.identity).ticket.userdata;

      return serialize.decrypt<userinfo>(struser);        
    }
  }

  public loginbasepage()
  {
    //
    // todo: 在此处添加构造函数逻辑
    //
  }
}

loginbasepage : page,基类页要继承page,成为所有登录以后的页面的基类。

属性protected userinfo loginuser{ get;}用来访问登录信息。将context.user.identity强制转换为formsidentity类的对象,通过访问ticket属性的userdata属性,获得被序列化后的对象的字符串,最后用方法serialize.decrypt<userinfo>(struser)将字符串反序列化成对象后再返回userinfo类型的对象。

我们只需要将default页面的后台代码改为public partial class _default : loginbasepage,就可以通过this.loginuser来访问用户登录信息了。

第四步:实现不同目录的权限控制

上面,实现了记录用户登录信息的模拟登录过程,以及根目录下文件的访问控制。但是系统一般都会有多个目录,接下来就说说目录的访问控制。

其实,上面多多少少已经提到过了,通过在每个目录下增加web.config文件来进行访问限制。

首先,我们在根目录增加一个文件夹manageadmin,在此文件夹内增加页面userinfo.aspx,页面内放几个label用来展现登录用户信息。

然后,再增加一个web.config文件,配置内容如下:

<configuration>
  <appsettings/>
  <connectionstrings/>
  <system.web>
    <authorization>
      <allow users="admin"></allow>
      <deny users="*"></deny>
    </authorization>
  </system.web>
</configuration>

配置中说明只允许“admin”用户访问,禁止其他所有用户访问。

这里,特别要注意的是,formsauthenticationticket票据的name属性的赋值,一定要和<allow users="admin"></allow>设置的用户想对应,且大小写敏感。如果要设置允许多个用户访问,则用“,”隔开,例如<allow users="admin,user1"></allow>。

不同的目录,设置不同的允许访问的用户,就可以对所有目录进行访问控制了。

第五步:实现不同目录的按角色的权限控制

以上实现了对不同目录按用户的访问限制。但是一般来说,一个网站系统的用户会很多,如果一直使用精确到用户的访问控制,则会造成设置web.config的工作量加大。

而一般,我们会将用户分到不同的用户组来进行权限控制,因此,我们也可以配置web.config实现按角色来控制不同的目录的访问权限。

首先,我们在根目录下再增加一个目录manageusers,在此文件夹内也增加页面userinfo.aspx用来展现登录用户信息。此目录将模拟控制users组的用户,文件夹manageadmin将模拟控制administrators组的用户。

然后,在目录manageusers增加web.config文件,配置内容如下:

<configuration>
  <appsettings/>
  <connectionstrings/>
  <system.web>
    <authorization>
      <allow roles="users"></allow>
      <deny users="*"></deny>
    </authorization>
  </system.web>
</configuration>

再将文件夹manageadmin下的web.config文件的<allow users="admin"></allow>改成<allow roles="administrators"></allow>

最后,修改代码。

1、注意,我们在模拟用户信息的时候,有这么一句,user.roles = "administrators,users";也就是用户admin具备两种角色

2、为模拟users组的用户登录,我们再添加如下代码:

if (this.textbox1.text == "user1" && this.textbox2.text == "111111")
{
      // 加密userinfo
      userinfo user = new userinfo();
      user.id = 2;
      user.name = this.textbox1.text;
      user.password = this.textbox2.text;
      user.realname = "普通用户1";
      user.roles = "users";
      string struser = serialize.encrypt<userinfo>(user);

      // 设置ticket信息
      formsauthenticationticket ticket = new formsauthenticationticket(
        1, user.name, datetime.now, datetime.now.addminutes(20), false, struser);

      // 加密验证票据
      string strticket = formsauthentication.encrypt(ticket);

      // 使用新userdata保存cookie
      httpcookie cookie = new httpcookie(formsauthentication.formscookiename, strticket);
      cookie.expires = ticket.expiration;
      this.response.cookies.add(cookie);
      

      this.response.redirect("default.aspx");
 }

这样,我们登录时,输入“admin”和“user1”的时候,就可以模拟不同角色的用户登录了。

3、forms基于角色的验证的内部机制是,将角色的属性也设置到了context.user中,这里也需要手工代码处理一下。  

首先,为了支持基于角色的验证,我们每进入一个页面都需要将角色信息设置到context.user中,那么最好的办法就是在global.asax 文件中的application_authenticaterequest方法中设置。

application_authenticaterequest方法,是在每次验证请求时触发,它与另外一个方法application_beginrequest的区别就在于,application_authenticaterequest方法内,能够访问context.user.identity,而application_beginrequest则无法访问。

我们在根目录添加一个global.asax 文件,增加如下代码:

protected void application_authenticaterequest(object sender, eventargs e)
  {
    if (this.context.user != null)
    {
      if (this.context.user.identity.isauthenticated)
      {
        if (this.context.user.identity is formsidentity)
        {
          string struser = ((formsidentity)this.context.user.identity).ticket.userdata;

          string[] roles = serialize.decrypt<userinfo>(struser).roles.split(',');

          this.context.user = new genericprincipal(this.context.user.identity, roles);
        }
      }
    }
  }

此处,主要代码就是将context.user.identity强制转换为formsidentity类的对象,通过访问ticket属性的userdata属性,获得被序列化后的对象的字符串,最后用方法serialize.decrypt<userinfo>(struser)将字符串反序列化成对象,再将userinfo对象的roles属性以“,”为分隔符分隔成角色数组,再用context.user.identity和角色数组生成一个新的genericprincipal对象,赋值给context.user ,则context.user 为已经设置好角色的验证对象。

按照我们的设置,admin用户能访问两个目录,而user1用户,则只能访问manageusers一个目录。

第六步:集中管理web.config文件

目录的访问权限控制,是按用户还是按角色,一般由具体业务决定。

但是,随着目录的增多,每个目录下都存在一个web.config文件,管理起来特别不方便。

通过上面提到过的location节的path属性,我们可以实现web.config配置的统一管理。我们可以将各个文件或目录的配置都放置在根目录的web.config文件内,代码如下:

<configuration>
  <appsettings/>
  <connectionstrings/>

  <location path ="register.aspx">
    <system.web>
      <authorization>
        <allow users="*"/>
      </authorization>
    </system.web>
  </location>

    <location path ="manageadmin">
    <system.web>
      <authorization>
        <allow roles="administrators"></allow>
          <deny users="*"></deny>
      </authorization>
    </system.web>
  </location>

    <location path ="manageusers">
    <system.web>
      <authorization>
        <allow roles="users"></allow>
          <deny users="*"></deny>
      </authorization>
    </system.web>
  </location>

    <system.web>
    <!-- 这里放置原来根目录 web.config 的内容,就不列出来了 -->
     </system.web>

</configuration>

结尾:

这次彻底理顺form验证的过程,发现了不少实用性很强的技巧,中间参考了很多网友的文章,也通过reflector看了一下具体实现的源代码。感觉收获不少,最大的收获就是对于问题不但要知其然更要知其所以然,要有一种打破沙锅问到底的净胜。

大家如果有什么问题有什么疑问,不但要找到解决的办法,有时间的话最好从理论到底层代码都好好过一过,对自己的水平长进有很大的帮助。

以上这篇asp.net实现form认证的一些使用技巧(必看篇)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。