ASP.NET安全问题--ASP.NET安全架构
在开发Web程序中,我们可以选择用自己的方法来实现安全的策略,或者可以购买第三方的安全代码和产品,不管怎么样,都是要很大的花费的,幸好在.NET Framework中已经内置了安全的解决方案。 ASP.NET和 .NET Framework 联合IIS为Web应用程序安全提供了一个基础结构。它的一个很明显的优势在于我们不必再编写自己的安全架构,我们可以利用.NET安全架构的内置的特性,而且整个安全的架构是经过测试和时间的考验了的。
.NET安全架构包含了很多的类,这些类用来处理身份验证,授权,基于角色的授权,假冒(Impersonation),代码访问安全,还包含了一个用于构建自定义解决方案的基本架构。
下面就开始:
一 ASP.NET实现安全的过程
ASP.NET 安全架构分为几个关键的安全过程:身份验证,授权,假冒,加密提供提供了必需的功能。具体看看一些解释:
身份验证--指明是谁再访问我们的站点
授权----谁可以对哪些资源操作和访问?访问站点的用户是否被授权使用他所请求的资源?
假冒----准备假冒什么角色?(注:假冒不是贬义词,不是我们常说的假冒商品的假冒,因为不同的用户角色有不同的权限,如果 我们当前的用户无法访问某一特定的资源,我们就可以让想访问特定资源的用户假冒,更确切的说是模仿有权限访问特定资源的用户,简言之:用户A想访问C资源,但是没有权限,但是用户B可以访问,所以A和B商量,A就用B的身份访问。具体的以后讲解)
下面我们具体看看每个安全的过程:
1.身份验证
身份验证是揭示用户标识(注:标识的概念我们后面马上就讲的,简言之,用户的ID 和 名称)并判断标识真实性的过程。很好理解,举个例子(大家注意例子中的一些术语):我们要取参加一个会议,我们就会取登记提供我们的一些证件即标识(表明我们的身份),一旦标识被确认,我们就会得到会议通行证,我们就可以带着通行证参加会议。而且会议中的每个人都可以通过我们的通行证了解我们的一些信息,如我们的名字,公司。身份验证就是:一旦标识被确定,我们就会得到一个可以识别我们的令牌,所以,再一个特定的区域内,不管我们在哪里,我们的标识都可以被识别。
在ASP.NET中,有4中身份验证的模式:
Widows身份验证(Windows Authentication)
Forms身份验证(Form Authentication)
Passpot身份验证(Passport Authentication)
自定义身份验证
对于每一种身份验证,用户都需要在登录的时候提供凭证,一旦标识被核实,用户就会获得一个身份验证令牌,在Forms验证中,整个令牌就是FormsAuthenticationTicket,整个令牌就放在 cookie中,每次请求资源的时候,令牌就会提供用户的标识信息。
2.授权
我们接着拿之前的那个会议的例子来看,授权就是表明我们可以做什么。进入会议厅以后,发现有很多不同的会议,专家级的,普通级的,不同人参加不同级别的会议。而且有些人可以参观整个会议厅,但是有些人只能在展览厅参观。这就是权限的不同而导致的。
所以,授权就是:以我们的标识信息为参考,批准或者拒绝访问我们请求的资源。还有一点要注意的是:我们一般是常用的是基于角色的授权,就是把用户分为一组一组,然后给每组不同的角色。
假冒
假冒是在其他用户标识的上下文中执行代码的过程。在默认情况下,所有的ASP.NET代码都是在Domain/ASPNET用户账户下执行的,要利用其他的标识执行代码,假冒其他的标识,我们应该利用.NET安全架构中的内置的假冒的功能。它允许我们指定执行代码的用户账户,比如不同于Domain/ASPNET的预定用户账户。我们既可以利用ASP.NET中身份验证功能来验证用户,也可以利用标准的Windows身份验证来验证用户。
然后我们可以利用我们的凭证,或者利用执行代码的预定义用户账户来设置所假冒的账户。
假冒还允许我们在不使用ASP.NET提供的身份验证和授权功能的情况下提供身份验证和授权:我们可以利用用户账户和他们相关权限支持Windows和IIS管理身份验证和授权。
假冒通常用于提供访问控制,比如授权,一个应用程序可以访问它所假冒的的用户可以访问的任何资源。例如,默认情况下,Domain/ASPNET用户不能对文件系统进行读写操作的,所以这个用户账户也无法在Enterprise Services中执行事务处理。但是利用假冒,用户就可以通过假冒一个特定的Windows账户完成这些事情,因为这个特定的账户有这个权限。因此,我们就可以保证一些用户可以对文件系统进行读写操作,而其他的一些用户仅仅执行读的操作。
好了,上面讲了很多,我们现在就来小结一下,看看如何把身份验证,授权,假冒一起用于Web程序中。
当用户首次访问Web站点时,他们是匿名用户,我们不知道他们的标识,除非对他们进行身份验证,否则我们以后还是不知道他们的标识。当用户请求非安全的资源时,他们可以自动的访问这个资源(这就是非安全资源的定义)
当用户请求安全的受保护的资源时,就要如下步骤:
1.请求被发送到 Web服务器,由于此时这个用户标识还有被确认,所以用户就被重定向到登录页面
2.用户提供凭证,身份验证就对凭证进行验证和审核
3.如果用户凭证合法,就可以访问资源,否则,就不能。
当用户请求安全的资源,但是该资源有特定权限的用户才能访问,就会发生下面步骤:
1.请求被发送到 Web服务器,由于此时这个用户标识还有被确认,所以用户就被重定向到登录页面
2.用户提供凭证,身份验证就对凭证进行验证和审核
3.把用户的凭证或者角色与被允许的用户或者角色进行比较,如果用户在列表中,那么他们就被准许访问这个资源,否则,拒绝。
如果启用了假冒,那么在这两种情况下,假冒都会发生。默认情况下,假冒是禁止的,可以修改配置文件添加<identity>元素启用:
<configuration><system.web>
<identity impersonate="true" userName="Xiaoyang/User" password="xiaoyang"/>
</system.web> </configuration>
在<identity>中,把impersonate特性设置为true,拿userName和 password设为要假冒的用户账户。 如果假冒被启用,那么被审核的就是假冒的用户标识的凭证,而不是提交的凭证。这两种凭证有可能相同,需要注意的是:假冒是利用Web服务器中已有的用户访问,如IUser用户。
ASP.NET安全架构为实现Web应用程序的安全模式提供了对象模型。不管我们选择哪一种的身份验证模式,其中很多的要素是相同的。登录到应用程序的用户按照他们提供的凭证被授予Principal和Identity。其中Principal对象表示的是用户的当前的安全上下文,包括用户的标识和他们所属的角色。Identity对象表示的是当前用户。Principal对象是利用Identity对象(表示用户的标识)创建的,而且它添加一些额外的信息,比如角色或者自定义的数据。
简言之:Principal=Identity+角色+自定义数据
大家要注意一点:身份验证是发生在ASP.NET运行的特定的时期的,记住这一点,具体的以后会讲的。
一 表示安全的上下文
Identity对象表示的是通过身份验证的用户。Identity对象的类型取决于所使用的身份验证,如,Windows身份验证使用的是 WindowsIdentity对象,而Froms验证使用的是FormsIdentity对象。
大家开始接触Identity概念有点难受,其实说白了Identity就是一个用户的标识,标识是什么?就是标明用户是什么,用户名字是什么而已,只是我们这里说的比较专业一点点而已。
另外,Principal对象表示的是通过身份验证的用户的组或者角色的成员:也就当前用户安全上下文。说安全上下文,说白点就是这个对象包含很多的用户身份的一些信息。Principal对象是有IIS中的Windows 身份验证自动的创建的,但是我们也可以创建普通的Principal对象(大家后面就慢慢明白的).
大家在编程的时候,或多或少用过HttpContent.Currrent.User属性,其实它表示的就是一个Principal对象.Principal对象是实现了IPrincipal接口的。
IPrincipal 接口
不同的身份验证模式对安全的上下文有不同的要求。我们可以利用Principal对象表示当前的安全上下文。IPrincipal接口定义了Principal对象的基本功能。而且我们还可以自定义安全的上下文对象,只要实现System.Security.Principal接口:
Identity属性--可以获取当前Principal对象的Identity.之前说过:Principal包含Identity就是这个原因。
IsInRole(string roleName)方法--可以判断当前的Principal对象是否属于指定的角色。大家在变成时候也用过类似的HttpContent.Current.User.Identity.IsInRole("Admin")语句。
Principal对象可以通过HttpContent.Current.User属性访问到,下面的代码大家应该都用过的:
if(HttpContext.Current.User.Identity.IsAuthenticated) {
lblUserName.Text=HttpContext.Current.User.Identity.Name+"已经登录";
}
(注:Identity是用户的标识,包含用户名。我们后面会讲的)
下面的代码就更加的常见了:判断当前的用户是否是管理员角色
if(HttpContext.Current.User.IsInRole("Admin"){ // }
接着我们就看看ASP.NET内置的实现了IPrincipal接口的对象:
GenericPrincipal类
GenericPrincipal类实现了IPrincipal接口。从名字可以看出GenericPrincipal对象表示的是一个一般的,基础的安全上下文,它仅仅只是定义了当前用户的角色,也就是说这个对象只是部分的实现了IPrincipal接口。(以后我们就把实现IPrincipal接口的对象称为 主体)。对于一种身份验证模式来讲,如 Windows身份验证,它使用的就是WindowsPrincipal,因为WindowsPrincipal更加具体的实现了IPrincipal。而在Forms验证中,用的只是一般的GenericPrincipal。也就是说,我们可以按照我们的要求实现自定义的Principal对象。下面会讲到的。
IPrincipal接口的每一个实现都要重写Identity属性和IsInRole方法。GenericPrincipal类的IsInRole方法是通过把角色值和在字符串在中定义的角色进行比较,而WindowsPrincipal类的IsInRole方法则是把角色和被分配到Windows用户帐户角色进行比较。
我们可以创建一个在当前请求的整个生命周期中都要使用的GenericPrincipal类的实例,并把它赋值给HttpContent.Current.User属性。
GenericPrincipal的构造函数有两个参数:用户的GenericIdentity(用户标识GenericIdentity实现了IIdentity接口),和一个表示用户角色的字符串数组。所以我们之前说:Principal=Identity+角色就是这原因。
GenericPrincipal 对象一旦被创建,就可以赋值到HttpContent.Current.User属性,用来表示当前请求用户的安全的上下文。
下面就是创建的代码例子:
//创建一般的GenericPrincipal //我们说过:标识就是包含用户名的对象,如下 包含一个名为"xiaoyang"的标识
GenericIdentity identity=new GenericIdentity("xiaoyang");
//创建GenericPrincipal
//注roles表示的是一个角色的字符串数组如role=new string{"Admin","Customer"};
GenericPrincipal principal=new GenericPrincipal(identity,roles);
//附加
HttpContext.Current.User=principal;
注意:上面的代码是要写在特定的地方,也就是生命周期的特定的时候的,我们后面讲述。
说了Principal,下面就说说用户标识到底是什么,之前多次提到的。
二 用户标识
Identity对象用于标识当前用户的标识。标识只能提供少量的安全上下文信息,如用户名。Identity对象可以验证用户。
IIdentity接口
和IPrincipal接口一样,表示用户标识的对象都要实现这个接口。IIdentity接口定义了Identity对象的基本额的结构,定义如下:
AuthenticationType(string类型的)属性--它可以获取所使用的身份验证的类型,如,如果使用的Forms验证,该属性返回"Forms"字符串,所以我们自定义的标识可以返回"CustomIdentity"字符串。
IsAuthenticated(bool类型)属性--标识用户是否通过身份验证。我们可以常常用HttpContext.Current.User.Identity.IsAuthenticated来判断用户是否已经登录。
Name(string 类型的)属性--获取用户的名字。相信对HttpContext.Current.User.Identity.Name不陌生。
下面我们就看看我们自己的实现了IIdentity接口的标识类。
using System;
using System.Security.Principal;
public class CustomIdentity : IIdentity
{
private string name;
//构造函数只接收一个string参数,大家可以看看之前我们代码:GenericIdentity identity=new GenericIdentity("xiaoyang");
public CustomIdentity(string name){ this.name = name; }
private string authenticateType = "CustomerIdentity";
public CustomIdentity(string name,string authenticateType) {
this.name = name;
this.authenticateType = authenticateType;
}
//下面就实现接口
private bool isAuthenticated = false;
public bool IsAuthenticated { get { return isAuthenticated; } }
private string name;
public string Name { get { return name; } }
}
上面的代码只是示范,大家可以按照自己的要求扩展。
和之前一样,我们来看看ASP.NET中内置的Identity类:
FormsIdentity--在Forms验证中使用
PasswordIdentity--在Passport验证中使用
GenericIdentity--一般的普通的标识
WindowsIdentity--Windows 身份验证使用
我们来看看GenericIdentity的使用,其他的使用类推。
其实GenericIdentity标识的是一个基本的Identity 对象。它对于Identity对象来说是做基本的。之前我们看过一个GenericPrincipal的例子,在那个实例中我们创建一个GenericIdentity类的实例,
GenericIdentity identity=new GenericIdentity("xiaoyang");
而且我们还可以提供更加具体的Identity对象,如之前提到的FormsIdentity,开提供具体的用户信息。
ASP.NET生命周期中的验证以及身份验证模块
其实在ASP.NET中每一个请求都进行了验证和授权的。进行验证和授权的过程实际上是通过触发相应的事件来完成的。
在讲述验证事件之前,首先清晰一个流程:ASP.NET运行时接到一个请求的处理的流程。
先把流程描述一下,使得大家有个总体把握:一个请求来了,经过IIS,通过ISAPI,就到达了ASP.NET的管道中,然后经过一些的转化和包装,然后ASP.NET运行时开始处理这个请求了,然后是进行验证和授权,然后再进行一系列的处理,最后确定请求是是什么文件,如果是.aspx的,那么然后就开始页面的生命周期,如下图。
下面就处理请求时候触发的事件顺序如下:
BeginRequest: 开发处理请求,是处理ASP.NET请求时触发的第一个事件
AuthenticateRequest:处理身份验证
...
AuthorizeRequest:处理授权
...
所以大家可以看出,其实在请求的处理过程中,身份的验证和授权发生的时期是很早的。而且有关验证的一些信息,如用户名和角色在处理完这两个事件之后就已经确定,并且填充。下面我想用个图来讲述:
一般对于请求的验证和授权,我们是希望也应该自己控制这个过程的,所以我们可以在AuthenticateRequest和AuthorizeRequest的事件处理中加入我们自己的代码。一般在网站中加入 Global.asax文件。
AuthenticateRequest事件:
当一个请求需要进行身份验证时,HttpApplication对象就会触发AuthenticateRequest事件,这也意味着每次对ASP.NET应用程序进行页面请求时都会触发这个事件。
在这个事件中实际上是调用相应的身份验证模块来处理身份验证的。例如对于Forms身份验证模块而言,就是从加密的cookie中提取用户信息。
AuthorizeRequest事件:
AuthorizeRequest事件是在AuthenticateRequest事件中通过了身份验证时候才触发的。AuthorizeRequest事件调用相应的授权模块来检查用户是否被授权访问他们请求的资源。因为在AuthenticateRequest事件完成之后就已经有了用户的表示Identity和IPricipal,也就知道了用户名和角色的信息。
2.身份验证模块
身份验证是利用ASP.NET中的身份验证模块来实现的。在ASP.NET中内置有四个验证模块:
DefaultAuthenticateModule,FormsAuthenticateModule,WindowsAuthenticateModule,PassportAuthenticateModule.他们都实现了IHttpModule接口,当然,如果需要,我们也可以实现自定义的验证模块。
从验证模块就可以看出我们一般的身份验证是怎么进行的,决定采用个验证模块是由我们决定采用哪种身份验证决定的,如Forms验证就是用了FormsAuthenticateModule来处理的。而且还要配置web.config文件
中的<authentication/>元素。
下面我们就先来看看身份验证模块的实现(伪码)
public class FormsAuthenticationModule:IHttpModule
{
#region IHttpModule 成员
public void Dispose(){ throw new NotImplementedException();}
public void Init(HttpApplication context) {
context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest);
}
public event FormsAuthenticationEventHandler Authenticate;
#endregion
}
大家可以看到,其实在模块的Init方法中就订阅了AuthenticateRequest事件,而我们在Global.asax中写的代码就是我们具体处理的代码:
void context_AuthenticateRequest(object sender, EventArgs e){
//自定义验证代码
}
还有一点要注意的就是:每个模块都有一个Authenticate事件。
下面就说说常用的两个个验证模块,因为大同小异:
WindowsAuthenticateModule:
WindowsAuthenticateModule是和IIS身份验证联合试用的,当web.config 的中配置如下:
<authentication mode="Windows"/>
此时,这个模块就激活了,在Global.asax文件中就的Application_Authenticate(object sender,eventArgs e)就是给Windows验证用的。
这里有一个问题要澄清:如果是我们配置的是Windows身份验证,我们在Application_Authenticate中就只能写和Windows身份验证有关的代码,如果是配置的是Forms验证,我们在Application_Authenticate就只能写和Forms有关的代码,如获取cookie信息等。也就是说,Application_Authenticate方法是"一法多用"。
接着谈 Windows验证,在上面的处理程序中,我们可以创建自己的用户信息,如我们创建一个WindowsPrincipal类的实例(实现了IPrincipal接口,包含用户名和角色的信息),然后它赋值给 HttpContext.User属性。这正如我们之前说的,验证事件执行完之后,我们就知道了这个请求的发起者的用户名和角色信息。
FormsAuthenticateModule:
首先还是要配置:<authentication mode="Forms"/>.
FormsAuthenticateModule可以利用cookie解析保存在cookie中的用户的信息,并且创建一个GenericPrincipal,把用户名和角色信息保存在其中,然后赋值给User属性,以备后用。
3.授权模块
在ASP.NET中内置的授权模块主要有两个:FileAuthorizationModule和UrlAuthorizationModule。他们也实现了IHttpModule接口。这些模块可以参照所试用的身份验证类型来决定到底采用哪个授权模块:
如果试用的是Windows身份验证,那么在授权检查的时候就会使用FileAuthorizationModule;
如果在web.config中提供了<authorization/>元素,那么就会采用UrlAuthorizationModule。如下面的:
<authorization><allow roles ="" users=""/><deny users=""/></authorization>
FileAuthorizationModule:
如果使用 Windows身份验证,就会采用FileAuthorizationModule模块。这个模块可以处理Authorization事件,并且能够对IIS提供的请求的令牌和目标资源执行访问检查。而且这也用到了系统的ACL(访问控制列表).
例 如,如果请求的资源是Default.aspx,当前的用户是xiaoyang,那么FileAuthorizationModule就会执行访问检查,看看xiaoyang时候具备访问Default.aspx的读的权限,如果在Windows的用户账户中有xiaoyang这个账户,并且具有访问的权限,那么请求成功,否则,FileAuthorizationModule就把Reponse.StatusCode设置为401(未授权),之后请求就结束了。
UrlAuthorizationModule:
和上面的处理模块不一样,不管使用何种类型的身份验证,只要配置了web.config中的<authorization/>元素,就要使用UrlAuthorizationModule模块。这个模块在处理的时候执行如下:
1,把<authorization/>中声明的用户名和HttpContext.User.Identity进行比较
2.把<authorization/>声明的角色信息和HttpContext.User.IsInRole比较
如果比较成功就可以访问相应的授权的资源,否则把Reponse.StatusCode设置为401(未授权),之后请求就结束了。
ASP.NET安全问题--Forms验证的具体介绍(上篇)
1.基于角色的安全
相信大家多什么是角色的,以及基于角色的安全都有一些了解,他们的概念就不赘述了。在讲述基于角色的安全之前,希望大家对之前讲过的Identity和IPrincipal要了解(如果不是很清楚的,可以参看我之前的安全的系列文章)。
在.NET中,可以用.NET Framework灵活的将用户标识(Identity)和角色相结合,并且为结合后的主体定义权限。我们之前说过:
主体(IPrincipal)=(用户标识:包含用户名等信息的对象)Identity+角色;
我们可以定义很多的角色,然后为角色定义访问资源的权限。我们一般是定义角色,如定义一个Customer角色,然后使用户的角色为Customer,然后我们定义Customer的访问权限,那么这些访问的权限就应用到了那些角色为Customer的用户上,也就是说,我们并不是为每个用户分别定义权限,因为那样维护和修改起来麻烦。
当然,我们也可以对特定的用户单独的进行授权,如写入文件。授权的方式很多,但是他们的思想是大体相同的:
通常,一个应用程序的授权步骤如下:
1.判断用户是否有合法的访问资源的凭证(也就是我们之前谈论的验证的过程)
2.拒绝特定的用户访问特定的资源
3.允许特定的用户访问特定的资源
2. 权限的映射:
在.NET 中,有一些内置的Permission对象,它们可以使得用户有合适的权限来访问资源。在讲后面的问题之前,首先说说什么是Permission。我们知道,我们的系统中一般有很多的文件和文件夹,而且这些文件或者文件夹常常都是有访问的权限的,比如,我们可以把一些文件的权限定义为:只能管理员修改,一般的用户只读,这点大家都不陌生了。
注意:我们是通过系统来配置这些文件或者文件夹的权限的。(大家应该都会)
我们的网站,实质其实就是放在服务器上的一些文件,既然这些文件在服务器,如Win Server 2003上,那么这些文件肯定是有访问的权限的。如果你是这台服务器的管理员,那么你就可以对这些文件进行任意的操作,其他的用户就没有这个能力了。
现在我们假设,我们的网站是放在服务器上,而且网站的文件,如aspx页面,图片,App_Code等都在网站的文件夹中,而且这些文件资源的权限早就定义好了,如只读。
其实我们在.NET中的一些Permission类,其实就是权限的映射。怎么说?
假如我们的网站文件夹中的一个文件,如Admin.aspx页面,我们可以通过操作系统来定义这个文件的访问权限(操作为:选中文件,点击右键,选择"共享和安全",然后选择哪些用户可以访问,而且这些用户的操作是什么,如读,写,安全控制等)。这些都是系统定义的一些权限,.NET Framework就把定义在文件上的一些权限"取出",包装映射在一些Permission类中,这样我们就能用托管代码,如C#来操作这些权限,而不用Win API(非托管代码)来操作。(理解如有问题,欢迎大家指正!)
3.权限对象Permission简介
下面我们就举一些Permission类的例子.
FileIOPermission
PrincipalPermission
这些内置的权限对象都会保护指定的资源。如FileIOPermission对象将确保只允许授权的用户访问一个文件。即,FileIOPermission对象将当前用户的凭证映射在操作系统级别,映射的结果就是该用户和系统中已存在的某一用户的操作权限相同(如IUser),那么现在这个用户就可以设置文件的权限。
FileIOPermission
FileIOPermission对象要求用户是Windows用户,或者ASP.NET处理过程运行在一个特定的标识下(一般是ASPNET用户账户),并且根据为文件系统中的文件或者文件夹定义的权限,授予读或者写的权限。即,FileIOPermission对象在文件系统中定义的权限上下文中验证Principal.
例如,在ASP.NET程序中,我们可能想要在点击按钮之后写入一个文件,那么,文件就应该配置必需的访问权限,并且为登录到应用程序的用户授予必需的权限。
如果想要拒绝访问一个特定的文件夹,如C:\Windows,那么我们就可以通过在运行阶段拒绝对FileIOPermission对象操作的权限来实现。我们就可以在特定的地方写下:
[FileIOPermissionAttribute("SecurityAction.Deny,All="C:\\Windows")]
上面是以声明的方式写的,我们也可以用代码,以后讲述。
其中SecurityAction枚举定义了访问的类型,如拒绝Deny。
上面的声明可以在类级别,方法级别,以及程序集级别使用。这里只是简单的描述,大家知道就行了,具体的我们后面讲述。
PrincipalPermission
PrincipalPermission确保调用者的上下文具有与之相关联的被请求的Principal。这话有点绕,下面看个例子就明白了:
下面的代码确保:只运行名为"xiaoyang"的用户访问一个特定的方法:
[PrincipalPermission(SecurityAction.Demand,Name="localhost\\xiaoyang"]
ASP.NET安全问题--Froms验证的具体介绍(上篇)
我们知道,Forms验证主要是基于cookie的,说白一点就是:把用户信息保存在cookie中,然后发送到客户端;再就是解析客户端的发送了的cookie信息,进行解析,然后进行验证。关于cookieless的工作原理和方法,我这里不赘述,大家可以参看我的另外的一片文章:浅谈ASP.NET内部机制(一)。
当匿名用户请求一个需要验证后才能访问的资源和页面的时候,那么如果采用了Forms验证,那么URL授权模块就会把用户重定向到登录页面。而之前请求的URL就会被保存起来,等到用户正确的登录后,就再次转向之前要请求的页面。我想这点,大家应该都用过。
下面我们就看看登录的时候发生了什么,看看登录的具体的流程?也请大家注意我使用的一些术语,因为这些术语再Forms中都有特定的对象,大家之后就可以看到的,很重要。
1.再浏览器中有个登录窗体,要输入用户名和密码等凭证,通过提交给服务器的ASP.NET网站来审核,检查凭证是否正确。
2.如果凭证正确,那么就会再服务器端就会创建一个"身份验证票据"。身份验证票据中含有了经过加密的用户信息。
3.这个票据再服务器端被写入cookie中,然后发送到客户端。
4.然后用户就被重定向到他们最初请求的URL中。
注:大家可能会有疑问:最初请求的URL到底保存在哪里?不要担心,现在只要明白上面的流程就OK。
5.上面第4步就是要转向最初请求的URL,假设最初的请求页面是Default.aspx,那么现在就是从登录的页面Login.aspx转向到Default.aspx 页面,此时因为身份验证的票据cookie已经存在于客户端的浏览器中了,此时的转向Default.aspx页面时,实际是再次向服务器端发起了请求,所以正如我们之前所谈到的:每个请求都要从ASP.NET管道中一级级的向后传,要经历ASP.NET的的生命周期:Application_BeginRequest,Application_AuthenticateRequest.....。(希望大家明白)
但是这次的请求就和第一次我们发起的请求步同了,为什么?
第一次我们请求Default.aspx页面的时候,我们根本就没有提供任何的表明我们身份的票据,但是这次我们已经登录了,而且我们的浏览器中已经有了我们的身份验证的票据的cookie,此时在Application_AuthenticateRequest事件中,Forms验证模块就获取表明我们身份cookie,然后就利用cookie中信息填充Context.User。
验证模块处理完之后就是授权模块起作用了。其实URL授权模块就会利用我们之前填充在Context.User中的信息来验证用户是否被批准访问所请求的资源或者页面。
Forms验证中的API
实现Forms身份验证之前,我们看看组成Forms验证的API以及相关的类:
FormsAuthenticationModule:对每个请求进行验证的HTTP模块
FormsAuthentication:包含在Forms验证中我们常用的方法和属性(很重要的)
FormsIdentity:Forms验证标识。
FormsAuthenticationTicket:身份验证的票据,对用户的信息进行加密后的产物,我们一般把它写如cookie中,之前我们谈过了的。
上面的类在System.Web.Security下。
下面我们来一一介绍.
FormsAuthenticationModule
它是一个实现了IHttpModule接口的类。它可以用来处理每个请求的Application_AuthenticateRequest事件。如果发送了的请求中已经包含了cookie信息,那么这个模块就对cookie信息进行解密和解析,然后构造一个GenericPrincipal的类实例填充Context.User,并且也创建一个FormsIdentity的实例。
注意:当我们在web.config中配置了Forms验证后,那么我们在Application_AuthenticateRequest事件写的代码要是和Forms相关的API。上篇文章谈过了。
FormsAuthentication类
这个类很重要。
还有一点注意的就是:因为FormsAuthentication和FormsAuthenticationModule名称很相似,很容易混淆。
它们之前的区别在于,FormsAuthenticationModule是一个HTTP模块;而FormsAuthenticate是一个类,它有很多的方法和属性。更加直白的说就是:它们之前没有什么关联,只是在Application_AuthenticateRequest事件中我们常常要调用FormsAuthenticate类的一些方法和属性。而且FormsAuthenticate的很多方法都是静态的方法,我们不会创建FormsAuthenticate类的实例。
还有一点要特别注意的就是FormsAuthenticate的Authenticate方法。
我们之前说过了,我们一般是在登录窗体中提交用户信息,然后服务器端验证提交的信息,我们在服务器端常常是去数据库中检查这些信息的正确性,但是去数据库或者其他的数据存储(如文件,活动目录)中去检查只是一种情况。
还有另外的情况。不知道大家是否记得web.config 中的一个配置的节点:
<authentication mode="Forms">
<forms>
<credentials>
<user name="xiaoyang" password="xiaoyang"/>
<user name="panyan" password="panyan"/>
</credentials>
</forms>
</authentication>
如果我们在配置文件配置了上述的信息,那么我们就可以用Authenticate方法来检查提供了用户信息(用户名和密码)是否正确,如果我们没有在web.config配置用户的信息,也就是说我们是把信息保存在数据库等其他的地方,那么我们就不能Authenticate这个方法。当然我们很少用Authenticate这个方法,因为我们不可能把所有用户信息硬编码到配置文件中,但是还是要清楚这个方法。
另外我简单的介绍一些常用的方法,具体的使用我以后会讲述。
在FormsAuthenticate中使用频繁的是RedirectFromLoginPage方法。每当验证了用户的凭证后就会使用到这个方法,也就是我们之前说过的:跳转到我们最初请求的页面。
这个方法就这么简单的一"跳",但是其实在内部做了很多的事情:
1.为用户创建一个身份验证的票据
2.对身份验证的票据进行加密
3.创建一个cookie,把加密的票据保存在cookie中
4.向HTTP响应添加cookie,并且发送给客户端。
5.跳转,并且把用户重定向到最初请求的页面
另外FormsAuthenticate类还有很多的其他方法和属性:
FormsAuthenticate中涉及到客户端保存cookie的两个属性就是:
FormsCookieName:获取或者设设置cookie的名称
FormsCookiePath:获取或者设置cookie的url路径
其中FormsCookiePath属性有一点要注意:大多数的浏览器会在判断cookie是否要和请求一起发送时,用到cookie路径。(我们一般在配置文件配置path="/"),如果我们配置的path不是"/",那么这个cookie就不会和请求发送到服务器端.
FormsAuthenticate中和cookie操作相关的方法有:
Decrypt:提取身份验证cookie的加密信息,创建FormsAuthenticationTicket,也就是解密。
Encrypt:加密。从FormsAuthenticationTicket中获取信息,并且加密。以备我们之后把加密的信息写入cookie
GetAuthCookie:创建身份验证cookie,但是并不把它立即添加到HTTP响应中
SetAuthCookie:创建身份验证cookie,并且把它添加到Response.Cookie中。
RenewTicketIfOld:刷新身份验证cookie的生命周期
GetRedirectUrl:把用户重定向到他们最初请求的页面。
SignOut:使得当前的身份验证cookie过期。我们常用的注销功能。
FormsIdentity
大家现在应该知道什么是标识 Identity,它包含了用户名和ID标识信息,可以参看我前面的文章。
FormsAuthenticationTicket 票据
通过上面的讲解,大家已经对它不陌生了,FormsAuthenticationTicket实际上就包含用户信息的一个类的实例。
注意:FormsAuthenticationTicket和cookie之间的区别:
cookie其实就是一个载体,容器,它包含了加密后的FormsAuthenticationTicket。
FormsAuthenticationTicket类的UserName属性就是用户的用户名,我们可以根据这个属性识别不同的用户。
由于身份验证是基于cookie的,所以要考虑到cookie的过期的问题。比如我们在登录时有个"记住我"的checkbox,如果勾上,那么就创建了一个永不过期的cookie,处于安全,我不提倡这样。
所以在FormsAuthenticationTicket也提供了关于设置cookie属性:
Expiration:获取一个表示cookie期满的DateTime对象
Expired:判断cookie是否过期
IsPersistent:是否在用户关闭浏览器后继续保存cookie
IssueDate:返回最初设置cookie的时间
还有就是CookiePath:设置cookie的保存路径,前面谈论过了,一般设置为"/"。
另外FormsAuthenticationTicket身份验证票据目的是识别用户。同时,我们也可以利用FormsAuthenticationTicket的UserData属性添加额外的信息,如角色等,然后这额外的信息就可以保存在cookie中。
今天就谈这里。大家先有个总体的认识,具体的代码部分,我们后面谈。谢谢各位!!!
ASP.NET安全问题--Froms验证的具体介绍(中篇)
启用Forms身份验证
相信大家对很清楚如何启用 Forms验证,但是这里我还是罗嗦一下。
我们只要在 web.config文件中配置如下就行了:<authentication mode="Forms"/>
这样之后,ASP.NET运行时就知道我们启用了Forms验证,在生命周期的验证阶段就激活FormsAuthenticationModule模块。
还有一点要特别注意:<authentication/>元素节点只能在应用程序的根文件夹中的 web.config中配置。如果在子文件夹中使用这个节点就会报错。这也说明了每个应用程序只能定义一个身份验证类型。
而且在<authentication/>节点中,我们还可以进行更多的设置。如下:
<authentication mode="Forms ">
<forms name="MyCookie" loginUrl="Login.aspx" timeout="60"
path="/" protection="All">
</forms>
</authentication>
我们下面就看看<forms>节点中的属性的含义和用途:
name:定义身份验证cookie的名称。因为我们把身份验证的票据是存放在cookie中的,而且我们必须给身份验证cookie取具有唯一性的名称,防止和其他的应用程序中的cookie冲突。
loginUrl:从英文名字就可以知道是关于登录的。实际上就是标明把用户重定向到哪个页面取登录。
timeout:设置身份验证cookie持续的时间,以分钟为单位的。而且这个时间是一个相对的时间,就是说每次对用户进行验证时,cookie的期限就会被重新设置。如果在60分钟内用户再没有向服务器发起请求,那么cookie就过期,之后如果用户再次发起请求,那么就要重新输入用户名和密码等信息。
path:设置cookie的保存的路径,一般是设置为"/",我们不要轻易的改变。
我们之前也说过,我们的身份验证的票据是加密以后再存放如cookie中的,然后再把cookie发送到客户端。当客户端再次请求的时候,再服务器端就会解析客户端发送来的cookie信息,我们必须要确认:客户端发送来的cookie信息就是我们之前从服务器端发送过去的,也就是说,我们要判断,我们的cookie在客户端是否被篡改了。
所以,这就是<forms/>中属性protection的用途。protection的值有四个:
Encryption:对cookie的内容信息进行加密。
Validation:向cookie中的内容添加MAC(消息验证代码),以便服务器判断cookie是否被篡改了。
None:禁用加密和篡改检查
All:同时启用加密和篡改检查。
默认情况下是"All",因为它可以把数据加密放入cookie中,而且对返回到服务器端的cookie进行验证。
用户信息的存储
我们一般把用户的信息,如用户名和密码存放在数据库中。如之前一篇文章说的,我们也可以把用户的用户名和密码信息直接放在web.config文件中。
Forms验证其实对把用户凭证信息放在哪里提供了很大的灵活性。默认情况下是放在web.config中的。如下代码:
<authentication mode="Forms ">
<forms name="MyCookie" loginUrl="Login.aspx" timeout="60"
path="/" protection="All">
<credentials>
<user name="xiaoyang" password="xiaoyang"/>
<user name="panyan" password="panyan"/>
</credentials>
</forms>
</authentication>
见上面的 <credentials>节点,就是我们存放的用户信息。上面的信息是以明文(没有加密)的形式存放。我们也采用加密的方式存放,只要配置如下:<credentials passwordFormat="MD5">,配置 passwordFormat的加密方式就行了。那么用户在传输之前就会被加密。
还有就是把用户信息存储在数据库中,我们后面会讲述的,也是最常用的方式。
web.config配置的一些用法
1.在web.config中添加用户凭证
我们之前说过了,ASP.NET的身份验证中,对于如何存储用户凭证提供了很大的灵活性。默认情况下是把凭证存储在web.config中的。
我们还是来看看之前配置的一些节点信息:
<authentication mode="Forms ">
<forms name="MyCookie" loginUrl="Login.aspx" timeout="60"
path="/" protection="All">
<credentials>
<user name="xiaoyang" password="xiaoyang"/>
<user name="panyan" password="panyan"/>
</credentials>
</forms>
</authentication>
在<credentials>中就是我们存储的用户的一些信息。不知道大家是否还记得我们之前已经说过了的Authenticate方法:只有把用户信息存放在了配置文件中,我们才可以使用这个方法,代码如下(判断提交的用户信息是否合法):
bool IsAuthenticate= FormsAuthentication.Authenticate(userName, userPassword);
2.拒绝匿名用户访问
其实这是与用户授权的问题现关联的,关于授权问题,我们以后谈,这里只是简单的提下,算是预热吧!
我们可以拒绝匿名用户访问我们的网站,只要配置如下:
<authorization> <deny users="?"/> </authorization>
谈起这个节点,还是有一些话题的,这个我们在专门讲述授权的时候具体谈。现在我们的重点是验证!
Forms验证一些简单的使用
其实对于任何一种验证来说,无非就是提供输入用户信息的界面,也就是常见的登录页面。登录页面可以很简单:只要提供输入用户名和密码的输入框就行了,而登录页面的功能就是验证输入信息是否正确,如果正确就创建身份验证的票据并且保存在cookie中。
下面就看看一个简单的页面以及代码的实现:
注:界面很简单,大家可能会想到ASP.NET现有的Login控件,但是Login是基于MemberShip的。现在谈MemberShip还过早,因为我们后面文章专门的讲解从Forms验证到MemberShip的自然过渡,那样大家就可以很清楚的知道MemberShip到时怎么回事。
代码部分,其实主要是"登录"按钮背后的代码:
protected void btnLogin_Click(object sender, EventArgs e){
if (FormsAuthentication.Authenticate(txtUserName.Text, txtUserPasswork.Text)) {
FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, false);
}
else { throw new Exception("登录失败!!!");}
}
代码的前提是:我们把用户的信息存放在了web.config文件中,因为我们用了Authenticate方法。
首先代码就判断提供的用户名和密码是否正确,如果正确,那么就通过了验证,那么我们就调用RedirectFromLoginPage就把用户定位了他们最初请求的页面。
注意:当我们调用RedirectFromLoginPage方法时,RedirectFromLoginPage就创建一个身份验证的票据,即把用户的信息写入票据中,并且加密,并把票据写入cookie,然后就把用户定位了他们最初请求的页面。我们只是写了一行代码就实现了很多的功能。
还有一点就是RedirectFromLoginPage方法的第二个参数指出了是否应该创建一个持久性的cookie,即用户关闭浏览器后依然保留cookie。我们常常实现"记住我"的功能就是传入了true.
当我们登录了之后,要注销也只要一行代码:
protected void btnloginOut_Click(object sender, EventArgs e){
FormsAuthentication.SignOut();
}
Forms验证中的Cookie问题
之前我们所见到的示例都是利用非持久性的身份验证cookie来维持请求之前的身份验证。这就意味着:如果用户关闭浏览器,cookie就期满失效,下次还需要在此登录。从安全的方面还说,这是个好事。
身份验证cookie中保存的身份验证票据是ASP.NET应用程序识别用户的工具。例如,如果一个用户拥有一个管理员的有效验证票据,那么我们的ASP.NET网站就会认为这个用户就是管理员。
尽管使用持久化的身份验证cookie风险很大,但是还是有很多的用处的。例如,如果我们只是仅仅为了区别不同的用户,而不是对受限资源的访问进行限制,那么我们就不要求用户在每次访问站点时都登录,这样我们就可以利用持久化的cookie,而且创建持久化的cookie也很简单,如我们之前用的RedirectFromLoginPage,只要把方法的第二个参数设置为true就行了。
除此之外,创建持久化的身份cookie,我们还可以使用SetAuthCookie,并且把第二个参数设置为true。如下:
FormsAuthentication.SetAuthCookie(txtUserName.Text, true);
持久化的cookie只有在调用了FormsAuthentication.SignOut();方法之后才会过期。并且持久化的cookie不会受到web.config中的<forms>节点中设置的超时的影响。如果我们希望持久化的cookie超时,我们就要调用FormsAuthentication类的GetAuthCookie方法,设置cookie的期满日期和时间,并且我们自己手动的把它写入HTTP响应。
如下面的代码,我们创建一个时间为一周的持久化的cookie(代码加了注释)
protected void btnLogin_Click(object sender, EventArgs e){
//判断用户提供的验证信息是否正确
if (FormsAuthentication.Authenticate(txtUserName.Text, txtUserPasswork.Text)){
//创建一个持久化的身份验证cookie
HttpCookie cookie = FormsAuthentication.GetAuthCookie(txtUserName.Text, true);
cookie.Expires = DateTime.Now.AddDays(7); //设置cookie的有效时间
Response.Cookies.Add(cookie); //手动添加到HTTP响应中
//跳转到最初请求的页面
Response.Redirect(FormsAuthentication.GetRedirectUrl(txtUserName.Text, true));
}
else{
throw new Exception("登录失败!!!");
}
}
其中GetRedirectUrl获取用户最初请求的URL
验证流程讲述
我们首先假设一个场景:用户现在已经打开了我们的首页Default.aspx,但是有些资源只能是登录用户才可以看到的,那么如果这个用户想要查看这些资源,那么他就要登录。而且这个用户已经有了一个帐号。(我们本篇主要的话题是身份验证,至于创建用户账户是怎么创建的,我们不关心,方法很多,如直接一个数据库插入就行了!)
我们现在就把我们的一些流程说下:
1.用户登录,在输入框中输入用户名和密码信息
2.点击登录按钮后,到数据库中查询该用户是否存在
3 如果存在,服务器端代码就创建一个身份验证的票据,保存在cookie中,然后发送到客户端的浏览器
4.用户已经有了验证的cookie,那么就页面就跳转到用户之前请求的页面
数据库准备
那么下面我们就开始详细讲述:
首先,我们我们肯定要先得创建一个数据库,我们就取名为Login表,创建一个用户信息表,我们在在表中建立三个字段UserName,UserPassword,UserRole(大家可以创建更多字段,我这里只是演示,大家可以扩展的). 至于表中的数据,大家自己随便插入几条!
代码编写
因为我们常常要验证用户,所以我们把验证用户的代码写成一个方法放在App_Code目录下的Helpers.cs类中
代码如下:
public static bool ValidateUser(string username, string password)
{
然后我们就创建一个登录的页面Login.aspx,在页面上面放入两个TextBox,分别用来供用户输入用户名和密码。
放上一个按钮,用来登录。
回到Helpers.cs中,我再添加一个方法,来获取用户的角色:
public static string GetRoleForUser(string username ){
…
com.CommandText = “Select UseRole m Users Where Username=@Username;
com.Parameters.AddWithValue(“@Username”, username);
…….
string userRole= (string)com.ExecuteScalar();
}
为了启动Forms验证,我们还得到web.config文件中配置,如下:
<authentication mode=”Forms”>
<forms name=”.mycookie” path=”/” loginUrl=”Login.aspx” protection=”All”
timeout=”40” />
</authentication>
并且不允许匿名用户访问我们的网站 :
<authorization>
<deny users=”?”/>
</authorization>
然后我们就开始在Login.aspx的登录按钮下面写代码了:
基本思想如下:
1.验证用户是否存在,
2.如果存在,同时获取用户的角色
3.创建身份验证票据和cookie,并且发送到客户端的浏览器中
代码都加了注释,通过之前的基础,相信大家可以对下面的代码没有问题。
protected void LoginCallback(object sender, EventArgs e){
if (Helpers.ValidateUser(UserName.Text, Password.Text)){
string rolenames = Helpers.GetRolesForUser(UserName.Text); //获取用户的角色
//创建身份验证票据
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
UserName.Text, DateTime.Now, DateTime.Now.AddSeconds(40), false, roles);
string encryptedTicket = FormsAuthentication.Encrypt(ticket); //加密票据
//创建新的cookie
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName);
cookie.Value = encryptedTicket; //把加密后的票据信息放入cookie
Response.Cookies.Add(cookie); //把cookie添加到响应流中
//把cookie发送到客户端
Response.Redirect(FormsAuthentication.GetRedirectUrl(UserName.Text,false),true);
}
}
好了,现在如果我们正确的输入用户名和密码,那么我们的浏览器中就有了身份验证的cookie了,现在我们的页面就要马上从原来的Login.aspx转向到Default.aspx页面了,我们现在把这个转向的过程在头脑中把它慢速化,因为我们要分析这个过程。
在Login.aspx转向到Default.aspx页面跳转的过程中,其实我们在请求Default.aspx页面,这个我们之前请求的过程没有任何的区别,也是一样要经历ASP.NET的一些生命周期,但是这次我们的浏览器中已经有了身份验证的cookie,ASP.NET运行时在处理,在处理Application_AuthenticateRequest事件时就要解析我们的cookie了。其实在之前我们登录之前,在这个事件代码中也解析了cookie的,只是那时候没有找到cookie而以。
Application_AuthenticateRequest事件的代码中,其实就是解析cookie,然后把用户的身份标识,并且把用户的身份信息保存起来:
void Application_AuthenticateRequest(object sender, EventArgs e){
HttpApplication app = (HttpApplication)sender; //获取身份验证的cookie
HttpCookie cookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie != null) {
string encryptedTicket = cookie.Value;//解密cookie中的票据信息
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encryptedTicket);
string[] roles = new string[]{ticket.UserData.toString()};//获取用户角色信息
FormsIdentity identity = new FormsIdentity(ticket); //创建用户标识
//创建用户的主体信息
System.Security.Principal.GenericPrincipal user =
new System.Security.Principal.GenericPrincipal(identity, roles);
app.Context.User = user;
}
}
我们看到最后一行代码:app.Context.User = user;,把用户的身份以及角色信息保存在了User属性中。
我们就可以在页面中通过如下方法判断用户是否登录了:
if (Page.User.Identity.IsAuthenticated)
用下面的方法判断用户是否属于某个角色:
if (Page.User.IsInRole("Admin")
其实这个我们之前讲过了的Identity,IPrincipal概念有关,不清楚的可以看看之前的文章!
代码到这里,今天也写完了,有关身份验证的问题就要讲完了,还差一个问题没有讲述:自定义身份验证以及开发自定义的HttpModule.
之后的的文章将讲述授权的问题。