授权是决定一个已验证用户是否拥有足够的权限执行特定操作的过程。可以是请求一个网页、访问一个由操作系统控制的资源(文件或数据库等)或执行一个应用程序特定任务等。
URL 授权
设置安全权限最直接的方式是对单独的网页、Web服务、下级目录进行设置。理想的情况下,Web 程序框架应当支持资源特定的授权,而无需更改代码和重新编译程序。ASP.NET 通过 web.config 文件中设置授权规则满足了这个需求。
UrlAuthorizationModule这一特定的 HTTP 模块执行这一规则。它对每一次请求进行检查以确保用户无法访问你已经进行了限制的资源。这个类型的授权称为 URL 授权,因为它只考虑两个细节:用户的安全上下文和用户试图访问的资源的 URL 地址。
<authorization>
<allow users="comma-separated list of users" roles="comma-separated list of roles"
verbs="comma-separated list of verbs"/>
<deny users="comma-separated list of users" roles="comma-separated list of roles"
verbs="comma-separated list of verbs"/>
</authorization>
只存在两种类型的规则:允许和禁止(但可有任意条)。verbs 特性创建只对特定类型的 HTTP 请求(GET、POST、HEAD、DEBUG)起作用的规则。
1. 针对特定用户授权
<authorization>
<!-- 拒绝所有未知身份的用户(?),通常用于强行验证 -->
<deny users="?"/>
<!-- 允许所有用户(*)访问 -->
<allow users="*"/>
<!-- 执行规则时,ASP.NET 从配置列表由上而下扫描,一旦匹配则扫描结束 -->
<!-- 因此下面的规则会允许所有用户访问,包括匿名用户 -->
<allow users="*"/>
<deny users="?"/>
<!-- 限制特定 3 个用户的访问权限,他们不能访问包含这个 web.config 文件的目录中的资源 -->
<deny users="?"/>
<deny users="dan,jenny,mattew"/>
<allow users="*"/>
<!-- 无法限制 jenny 用户,因为之前的规则已经匹配 -->
<deny users="?"/>
<deny users="dan,mattew"/>
<allow users="*"/>
<deny users="jenny"/>
<!-- 只允许 dan,matthew 访问 -->
<deny users="?"/>
<allow users="dan,mattew"/>
<deny users="*"/>
<!-- 当使用 Windows 验证时,用户名的格式会以 域名\用户名 或 计算机名\用户名,这点要注意 -->
<deny users="?"/>
<allow users="FARIAMAT\dan,FARIAMAT\mattew"/>
<deny users="*"/>
</authorization>
2. 针对特定目录授权
ASP.NET Web 应用程序可在不同目录层次结构的分布中,创建多个 web.config 配置文件并设置授权,以此来达到开放资源的访问和限定资源的访问。当你在下级目录中添加 web.config 时,它不应该包含任何程序相关的设置。
你不能在下级目录中设置验证规则,即 <authentication> 标签。通常,程序的所有目录都应该使用相同的验证系统,但是每一个目录可以拥有自己的授权规则。
理解授权流程最简单的方式:把所有规则当作一个单独列表,从请求的页面所在目录开始。如果所有规则都被处理而没有一个匹配,ASP.NET 就会开始读取上级目录的授权规则。以此类推,直到成功匹配一个规则为止。如果最后没有规则匹配,ASP.NET 最终匹配 machine.config 文件定义的 <allow users=”*”>。
<!-- 子目录下有这条规则 -->
<allow users="dan" />
<!-- 根目录下有这条规则 -->
<deny users="dan" />
这将很有趣。dan 用户可以访问子目录下的任何资源,但是却无法访问根目录下的资源。
3. 针对指定文件的授权
通常,通过目录设置文件的访问权限是最简洁和最容易的方式。不过,仍然可以通过添加 <location> 标签限制指定的文件:
<configuration>
<system.web>
...
</system.web>
<location path="SecuredPage.aspx">
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>
<location path="AnotherSecuredPage.aspx">
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>
</configuration>
4. 针对特定角色授权
为了让网站的安全机制易于理解且容易维护,用户经常按照角色进行分组。管理一个企业程序,它支持成千上万个用户,角色的价值就体现了。为每一个用户分别设置权限,非常辛苦且难以修改,极易出错,这么做也不太现实。
Windows 验证中,角色自动有效且自然的整合在一起,实际上就是 windows 的用户组。如 Administrator、Guest、PowerUser 等。也可也创建自己的用户组来代表程序相关的分类(比如 Manager、Contractor、Supervisor 等)。
实际上,针对角色的授权和之前针对用户的授权规则本质上是一样的:
<!-- 禁止所有匿名用户,允许2个特定用户,允许2个特定组, 拒绝所有其他用户 -->
<authorization>
<deny users="?"/>
<allow users="FARIAMAT\dan,FARIAMAT\mattew"/>
<allow roles="FARIAMAT\Manager,FARIAMAT\Supervisor"/>
<deny users="*"/>
</authorization>
基于角色的授权规则概念上非常简单,但在实际中比较复杂。这是因为授权规则可以重叠。比如,允许一个用户组,然后又明确禁止其中一个用户?或者换种情况,通过用户名允许这个用户,但禁止这个用户所在的组?
ASP.NET 只使用第一个匹配成功的规则,因此,规则的顺序是第一重要的!
看下面这个例子:
<authorization>
<deny users="?"/>
<allow users="FARIAMAT\mattew"/>
<deny roles="FARIAMAT\Guest"/>
<allow roles="FARIAMAT\Manager"/>
<deny users="FARIAMAT\dan"/>
<allow roles="FARIAMAT\Supervisor"/>
<deny users="*"/>
</authorization>
- 用户 mattew 被允许访问,无论他在什么组
- Guest 组所有用户被禁止。假设 mattew 也是 Guest 组,但对他无效,他已经优先匹配成功了
- Manager 组所有用户都被允许。唯一例外是同时属于 Manager 和 Guest 组的用户。
- 用户 dan 被禁止。除非他是 Manager 组的
- 任何属于 Supervisor 组的而又没有被之前规则明确禁止的用户都被准许访问
- 最后,任何其他用户都被禁止
在代码中检查授权
保证一个安全程序的另外一个步骤就是让程序在试图执行特定任务之前执行检查,为此,需要写些代码。
1. 使用 IsInRole() 方法
之前的系列文章介绍过,所有的 IPrincipal 对象都提供了 IsInRole() 方法判断用户是否属于一个组,这个方法接收一个字符串的角色名称作为参数。
表单验证时:
if (User.IsInRole("Supervisors"))
{
// Do nothing, the page should be accessed as normal because the
// user has administrator privileges.
}
else
{
// Don't allow this page. Instead, redirect to the home page.
Response.Redirect("default.aspx");
}
Windows 验证时(自定义组):
if (User.IsInRole(@"FARIAMAT\Supervisors"))
{ ... }
Windows 验证时(系统内置组)
if (User.IsInRole(@"BUILTIN\Supervisors"))
{ ... }
// 或者使用 WindowsPrincipal 对象的 IsInRole() 重载版本
WindowsPrincipal principal = User as WindowsPrincipal;
if (principal.IsInRole(WindowsBuiltInRole.Administrator))
{ ... }
2. 使用 PrincipalPermission 类
.NET 提供了另一种加强角色和用户规则的方式。使用 System.Security.Permissions 命名空间的 PrincipalPermission 类,而无需再用 IsInRole() 方法。
基本策略是创建一个表示你需要的用户或者角色信息的 PrincipalPermission 对象,再调用对象的 Demand() 方法。如果当前用户无法满足需要,会抛出一个 SecurityException 异常,你可以捕获这个异常(例如使用一个自定义错误页面处理它)。
PrincipalPermission 的构造方法有 3 个版本,其中前 2 个方法的参数从 2 - 3 不等,这些参数最终都会被 Demand() 求值:
- 用户名:
- 角色名:
- 一个标志,告诉 Demand() 方法验证用户是否经过验证(isAuthenticated)
最后一个构造方法接收唯一一个参数:PermissionState,这个参数继承自 PrincipalPermission 类的基类(这已超出本文的范围)。
可以省略任何一个参数,只需在相应位置提供一个空引用即可:
try
{
// PrincipalPermission: 允许使用为声明和强制安全性操作定义的语言结构来检查活动用户
PrincipalPermission pp = new PrincipalPermission(null, @"BUILTIN\Administrators");
// Demand(): 在运行时确定当前主体是否与当前权限所指定的主体相匹配
pp.Demand();
// If the code reaches this point, the demand succeeded.
// The current user is an administrator.
}
catch (SecurityException err)
{
// The demand falied. The current user isn't an administrator.
}
通常,PrincipalPermission 检查额外被用作失效保险的 web.config 规则。换句话说,调用 Demand() 方法可以保证即使 web.config 文件不小心被修改时,属于错误用户组的用户也不会被允许访问。
1. 合并 PrincipalPermission 对象(可按权限的并集和交集进行验证)
PrincipalPermission 的方法也可以使用更复杂的验证规则。比如,A 和 B 属于不同组,都被允许访问特定功能。如果使用 IPrincipal 对象,就需要调用 IsInRole() 两次。另一种方式是创建多个 PrincipalPermission 对象,把它们合并为一个 PrincipalPermission 对象,调用一次 Demand():
try
{
PrincipalPermission pp1 = new PrincipalPermission(null, @"BUILTIN\Administrators");
PrincipalPermission pp2 = new PrincipalPermission(null, @"BUILTIN\Guests");
// Combine these two permission
// Union(): 创建并返回一个权限,该权限是当前权限与指定权限的并集
PrincipalPermission pp3 = (PrincipalPermission)pp1.Union(pp2);
// Intersect(): 创建并返回一个权限,该权限是当前权限和指定权限的交集
//PrincipalPermission pp3 = (PrincipalPermission)pp1.Intersect(pp2);
pp3.Demand();
// Demand(): 在运行时确定当前主体是否与当前权限所指定的主体相匹配
pp3.Demand();
// If the code reaches this point, the demand succeeded.
// The current user is an administrator.
}
catch (SecurityException err)
{
// The demand falied. The current user isn't an administrator.
}
2. 使用 PrincipalPermission 特性
PrincipalPermission 特性提供了另外一种验证当前用户身份的方法,但异常处理的执行不同。必须在调用这个函数的调用者处捕获异常。具体看下面示例:
[PrincipalPermission(SecurityAction.Demand, Role = @"BUILTIN\Administrators")]
protected void Page_Load(object sender, EventArgs e)
{ ... }
在上面的例子中,异常捕捉就必须在全局的错误处理程序(Application_Error),Global.asax 文件中,因为你的代码不是这个网页的调用者:
void Application_Error(object sender, EventArgs e)
{
// 在出现未处理的错误时运行的代码
}
下面的例子则是限定特定角色才能使用的特殊方法:
[PrincipalPermission(SecurityAction.Demand, Role = @"FARIAMAT\finance")]
private void DoSomething()
{ ... }
这个方法的调用者可以用 Try..Catch 块捕获 SecurityException 异常。
PrincipalPermission 特性也给你另外一个保护代码的手段,也可以使用它们保证基本级别的安全,即使 web.config 的规则被修改或被规避。