ASP.NET 授权与角色 Part.2(使用角色 API)
ASP.NET 提供了一个现成的架构来管理和使用角色。和成员资格 API 类似,这个架构含有内置的功能,比如管理角色、给用户指派角色、从代码中访问所有的角色信息。这个架构也可以通过提供程序完全扩展。
详细来讲,角色架构包含如下内容:
- 一个基于提供程序的扩展机制,用来包含不同类型的角色数据存储
- 一个现成的 SQL Server 提供程序和必需的数据库表
- 预置的 RolePrincipal 类,它可以通过 RoleManagerModual 模块为已验证的用户自动初始化
- 使用 Roles 类,完全可以通过编程访问角色
1. 激活架构
首先,必须激活它。你可以在 ASP.NET 网站管理工具中的安全设置向导或者安全标签页为这个网站启用角色。这个工具会在程序的 web.config 文件中添加一些配置项,当然,你也可以手动完成这些。
同时,启用表单验证模式,此时的 web.config 文件内容如下:
<configuration>
<system.web>
<authentication mode="Forms" />
<roleManager enabled="true" />
</system.web>
</configuration>
只要有这个配置文件,ASP.NET 会自动创建一个基于文件的数据库 ASPNETDB.MDF。
如果要使用一个自定义的数据存储,就必须完成下面两个步骤:
- 通过 aspnet_regsql.exe 或 通过执行位于 .NET Framework 目录下的 TSQL 命令脚本创建数据存储
- 配置角色提供程序以使用前面的自定义存储
通过 <roleManager> 可以配置角色提供程序,你可以使用一个不同的数据库,也可以使用完全不同的存储机制。通过 <roleManager> 还可以配置一些特殊属性,即 WAT 中无法配置的属性:
<configuration>
<connectionStrings>
<add name="MySqlStore"
connectionString="data source=(local);Intergrated Security=SSPI;initial catalog=MySqlDB"/>
</connectionStrings>
<system.web>
<roleManager enabled="true"
defaultProvider="CustomSqlProvider"
cacheRolesInCookie="true"
cookieName=".MyRolesCookie"
cookieTimeout="30"
cookieSlidingExpiration="true"
cookieProtection="All">
<providers>
<add name="CustomSqlProvider"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="MySqlStore"
applicationName="Authorization"/>
</providers>
</roleManager>
<authentication mode="Forms" />
<compilation debug="true" />
</system.web>
</configuration>
一旦在 web.config 文件中添加了这些配置项后,就可以通过 WAT 选择提供程序了。只需要切换到提供程序标签页签即可:
<roleManager> 节可配置的选项
enabled | 是否激活角色 API |
defaultProvider | 可选特性。指定当前活动的提供程序来存储角色信息 |
cacheRoleInCookie | 不必每次都从后台存储读取角色,可将角色信息存放到 cookie 中 |
cookieName | 如果角色信息缓存在 cookie 中,可通过这个特性指定 cookie 的名字 |
cookiePath | 指定 cookie 的路径,这是应用程序缓存角色的地方,默认值是 / |
cookieProtection | 角色 cookie 可以被加密并签名。这个特性指定 cookie 的保护级别。有效值为:All(加密并签名)、Encryption、Validation、None |
cookieRequireSSL | 指定是否只有 SSL 被激活时(true)或在其他任何情况(false)下,由 ASP.NET 返回 cookie。 |
cookieTimeout | 获取或设置角色 cookie 的超时时间,单位是分钟,默认值 30 |
cookieSlidingExpiration | 指定在用户对 ASP.NET 请求时,是否延长 cookie 的超时时间。默认 true |
createPersistentCookie | 设为 true,cookie 在客户端永久保存。否则,关闭浏览器之后被删除 |
domain | 指定角色 cookie 有效域 |
maxCachedResults | 指定 cookie 中可以保存的角色名称的最大数量 |
在之前的例子中,配置使用了 SqlRoleProvider,这个提供程序包含一些额外的设置。
SqlRoleProvider 的额外属性
name | 提供程序的名字,可以用在 defaultProvider 特性中 |
applicationName | 管理角色的应用程序的名字 |
decription | 提供程序的简短且友好的描述 |
connectionStringName | 在 web.config 文件中 <connectionString> 节中指定连接字符串的名字 |
基础角色 API 类
类 |
描述 |
RoleManagerModule | 这个模块保证在每一次请求时,相应角色会被赋给当前登录的用户。它附着在 Application_AuthenticateRequest 事件上,并创建一个 RolePrincipal 实例 |
RoleProvider | 所有角色提供程序的基类。每一个自定义提供程序必须继承它 |
RoleProviderCollection | 角色提供程序的集合。允许你迭代配置的角色提供程序。当编写管理应用程序或页面时很方便 |
SqlRoleProvider | 一个基于 SQL Server 数据库的角色提供程序的实现 |
WindowsTokenRoleProvider | 获得一个基于 Windows 用户组经过验证的 Windows 用户的信息 |
AuthorizationStoreRoleProvider | 角色提供程序的实现,用来在一个基于授权管理器的数据存储中存储角色信息。Windows Server 2003 内置了授权管理器,它允许你定义程序的角色以及这个角色的权限。你的程序可以使用授权管理器通过编程对用户授权 |
Roles | 可以使用这个类作为你和角色存储的主要接口,这个类包含管理角色的编程接口 |
RolePrincipal | 一个 IPrincipal 的实现。把一个已验证用户连接到已配置的角色。只要角色 API 被激活,它就由 RoleManagerModule 自动创建 |
只要配置了角色 API,就可以创建用户和角色,并可以通过 WAT 或者程序中的 Roles 类把用户分配给这些角色。在“安全”页签上单击“创建或管理角色”链接,然后就可以创建角色并将用户添加到这个角色了。如下图:
配置了用户和角色之后,就要为程序配置授权规则。除了在程序不同目录配置合适的 <authorization> 之外,也可以在安全页签添加新的访问规则:
当激活了角色 API 之后,RoleManagerModule 会自动创建一个 RolePrincipal 实例。它包含这个经过验证的用户的身份信息以及他所属的角色。你可以使用下面代码中的属性从实例中提取信息,并调用 IsInRole() 方法来执行授权检查:
protected void Page_Load(object sender, EventArgs e)
{
// IsAuthenticated: 获取一个值,该值指示是否验证了用户
if (User.Identity.IsAuthenticated)
{
// RolePrincipal: 表示当前 HTTP 请求的安全信息,包括角色成员资格
RolePrincipal rp = User as RolePrincipal;
StringBuilder roleInfo = new StringBuilder();
// Identity: 为当前 HTTP 请求获取安全标识
roleInfo.AppendFormat("<h2>Welcome {0}</h2>", rp.Identity.Name);
// ProviderName: 获取存储和检索用户角色信息的角色提供程序的名称
roleInfo.AppendFormat("<b>Provider:</b> {0}<br />", rp.ProviderName);
// Version: 获取角色 Cookie 的版本号
roleInfo.AppendFormat("<b>Version:</b> {0}<br />", rp.Version);
// ExpireDate: 获取角色 Cookie 将过期的日期和时间
roleInfo.AppendFormat("<b>Expires at:</b> {0}<br />", rp.ExpireDate);
roleInfo.Append("<b>Roles:</b> ");
// GetRoles(): 获取 System.Web.Security.RolePrincipal 是其成员的角色的列表
string[] roles = rp.GetRoles();
for (int i = 0; i < roles.Length; i++)
{
if (i>0)
{
roleInfo.Append(", ");
}
roleInfo.Append(roles[i]);
}
lblRoleInfo.Text = roleInfo.ToString();
}
}
2. 和角色一起使用 LoginView 控件
之前的文章介绍过,LoginView 控件可根据是否为匿名用户显示不同的内容,使用 <LoggedInTemplate> 模板和 <AnonymousTemplate> 模板实现这个功能。这个控件还提供另一个模板 <RoleGroup> 来基于用户不同的角色显示不同的内容。
仔细观察下面的页面代码即可明白:
<asp:LoginView ID="LoginView1" runat="server">
<LoggedInTemplate>
<h2>This is the logged in template.</h2>
</LoggedInTemplate>
<RoleGroups>
<asp:RoleGroup Roles="Admin">
<ContentTemplate>
<h2>Only Admins will see this.</h2>
</ContentTemplate>
</asp:RoleGroup>
<asp:RoleGroup Roles="Others">
<ContentTemplate>
<h2>This is for others!</h2>
</ContentTemplate>
</asp:RoleGroup>
</RoleGroups>
</asp:LoginView>
注意:任何时候,这些角色模板只会显示能够匹配已登录用户角色的第一个模板!另外,LoggedInTemplate 模板只会为已登录且 RoleGroups 中没有任何模板被匹配的用户显示!
3. 通过编程访问角色
角色 API 允许你通过编程完成所有的任务。你可以通过编程添加新角色、读取角色信息、删除角色。还可以将用户分配给角色或者获得属于某个角色的用户。
通过 Roles 类的方法可实现这些功能。Roles 类大多属性是 <roleManager> 标签特性的映射。因此,下表只包含了一些额外的属性和 Roles 类的方法:
Roles 类的成员
Provider | 返回当前使用的提供程序 |
Providers | 返回系统上所有可用的提供程序。它会返回 machine.config 和 web.config 中定义的所有提供程序 |
AddUserToRole | 赋予一个用户一个角色 |
AddUserToRoles | 赋予一个用户一组角色 |
AddUsersToRole | 赋予一组用户一个角色 |
AddUsersToRoles | 赋予一组用户一组角色 |
CreateRole | 创建角色 |
DeleteRole | 删除角色 |
FindUsersInRole | 返回关联此角色的所有用户 |
GetAllRoles | 返回当前已配置的提供程序的角色存储中所有的角色 |
GetRolesForUser | 返回指定用户的所有角色 |
GetUsersInRole | 返回属于角色参数所指定的角色的所有用户列表 |
IsUserInRole | 指定用户是否属于指定角色 |
RemoveUserFromRole | 将一个用户从指定角色中移除 |
RemoveUserFromRoles | 将一个用户从所有指定角色中移除 |
RemoveUsersFromRole | 从一个角色中移除所有指定用户 |
RemoveUsersFromRoles | 从所有指定角色中移除所有指定用户 |
RoleExists | 是否存在指定的角色 |
编程访问角色的一个非常好的应用是,当用户注册时,可以自动将用户分配到相应的角色中。例如,如果程序提供了一个类似 Everyone 的角色,那么每一个单独的用户都应该是这个角色的一员。那么如何做到呢?
可以捕获 CreateUserWizard 控件的 CreatedUser 事件,但这还不够。因为 WAT 也可以创建用户,因此有必要再寻找一个补救的措施。可在全局程序 Global.asax 的 Application_AuthenticateRequest 事件中确认用户是否属于 Everyone 角色的成员之一来进行处理:
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (User != null)
{
// Roles: 管理角色中的用户成员资格,以便在 ASP.NET 应用程序中进行授权检查
if (User.Identity.IsAuthenticated && Roles.Enabled)
{
string everyoneRoleName = ConfigurationManager.AppSettings["EveryoneRoleName"];
if (!Roles.IsUserInRole(everyoneRoleName) && Roles.RoleExists(everyoneRoleName))
{
Roles.AddUserToRole(User.Identity.Name, everyoneRoleName);
}
}
}
}
从配置中读取 Everyone 角色的名称,因此角色名并没有被硬编码在应用程序中,这是个好习惯。
前面的代码中不能调用 User.IsInRole(),因为当应用程序范围内的 Application_AuthenticateRequest 被调用时,RoleManagerModule 还没有被调用。因此,含有用户和角色关联信息的 RolePricipal 还没有被创建。
4. 在 Windows 验证中使用角色 API
角色 API 还提供了一个提供程序 WindowsTokenRoleProvider,它与 Windows 角色集成用于 Windows 验证。它获取当前已登录用户所属的 Windows 组成员信息。你需要配置程序使用 Windows 验证,然后按下面的方式配置 WindowsTokenRoleProvider:
<configuration>
<system.web>
<authentication mode="Windows" />
<authorization>
<deny users="?"/>
</authorization>
<roleManager enabled="true" cacheRolesInCookie="false" defaultProvider="WindowsRoles">
<providers>
<add name="WindowsRoles" type="System.Web.Security.WindowsTokenRoleProvider"/>
</providers>
</roleManager>
</system.web>
</configuration>
RoleManagerModule 会自动创建 RolePrincipal 的一个实例,并将它和 HttpContext.Current.User 属性关联起来。用法和其他角色提供程序没有任何区别:
protected void Page_Load(object sender, EventArgs e)
{
if (User != null && User.Identity.IsAuthenticated)
{
RolePrincipal rp = User as RolePrincipal;
StringBuilder info = new StringBuilder();
info.AppendFormat("<h2>Welcome {0}</h2>", rp.Identity.Name);
info.AppendFormat("<b>Provider:</b> {0}<br />", rp.ProviderName);
info.AppendFormat("<b>Version:</b> {0}<br />", rp.Version);
info.AppendFormat("<b>Expires at:</b> {0}<br />", rp.ExpireDate);
info.Append("<b>Roles:</b><br /> ");
string[] roles = rp.GetRoles();
foreach (string role in roles)
{
if (!string.IsNullOrEmpty(role))
{
info.AppendFormat("-> {0}<br />", role);
}
}
lblRoleInfo.Text = info.ToString();
}
}
这种基于提供程序的架构允许你使用 Windows 验证和 Windows 用户组,而无需更改程序的内部逻辑。如果配置另外一个提供程序,你不需要改动代码,但是你需要做一些授权检查来检查程序中硬编码的角色名。因为 Windows 组包含域名限定词而自定义的角色没有。为了避免这样的事情发生,可以向程序添加一个功能,以允许你通过数据库或者配置文件来关联角色和权限,这由你的程序需求而定。
不建议在程序中直接使用 Windows 组来授权,除了几个内置的组之外,比如 Administrators 组。大多情况下,定义程序所需的角色是非常有帮助的,原因如下:
- Windows 组(非内置的组)依赖于它们所属的域或者计算机的名字
- 大多数情况下,一个域中的Windows组会根据企业组织和网络管理要求来分配组织,这些要求和程序的要求通常是不匹配的
- 独立于网络组的结构化程序角色可以让程序更灵活,可以在多种类型网络结构下使用
IIS 7.x 里的授权和角色
IIS 7.x 和 ASP.NET 一样原生支持基于 URL 的授权。IIS 7.x 发布了自己的 UrlAuthorizationModule。这允许在 <authorization> 配置项里把 URL 授权的配置作为 web.config 配置文件的 <system.webServer> 节的一部分。
下图是 IIS 中添加授权规则:
IIS 7.x 的 URL 授权完全和 ASP.NET 相互独立工作。web.config 文件里一个典型的 IIS 7.x 授权配置看起来会像是下面这样:
<system.webServer>
<security>
<authorization>
<remove users="*" roles="" verbs="" />
<add accessType="Deny" users="?" />
</authorization>
</security>
...
</system.webServer>
不要忘记,这个授权是由其自己的原生模块实现的,该模块随 IIS 7.x 一起发布,见下图:
理解原生模块的意义很重要。原生模块可以识别通过所有可能的验证模块验证的用户,包括 Basic 验证、Windows 验证、表单验证。因为这个模块被开发为能够正确理解表单验证票据(无论 cookie 或查询字符串中的编码)。不过遗憾的是,它实现的时候没有考虑 ASP.NET 角色。
这是为什么呢?角色由 ASP.NET 框架在应用程序生命周期的某个阶段从某个存储取得,然后角色信息被封装到实现了 IPrincipal 接口的托管对象,并因此保存在一个纯粹托管的 .NET 对象里,该对象不能被 IIS 7.x 原生模块访问。因此 IIS 7.x 原生模块不能使用该信息。
因此,不能把任何基于 ASP.NET 的角色和原生 IIS 7.x URL 授权模块一起配置到 <system.webServer> 节的 <authorization> 里。即:IIS 7.x 原生模块仅可适用于 Windows 角色主体名称。
在 IIS 7.x 里如何使用 ASP.NET 角色授权?
基于角色的授权是少数需要分别基于 IIS 和 ASP.NET 单独进行配置的例外之一。
现在我们知道原生模块并不识别 ASP.NET 特定的角色信息。但另一方面,我们也知道把 IIS 7.x 运行于 ASP.NET 整合模式提供了一个统一的 HTTP 处理管道,而原生模块和托管模块在同一个 HTTP 模块管道里处理。也就是说,你可以使用任意 .NET 语言编写的 HTTP 模块来扩展 IIS 7.x 的默认行为。你可以把 ASP.NET 的表单验证或 ASP.NET 的授权模块进行整合。
这样做能够方便的达成下面两件事:
- 保护非 ASP.NET 资源
- 为其他 Web 程序使用 ASP.NET 安全机制,即使它不是 ASP.NET 编写的
只要让随 ASP.NET 发布的托管模块 UrlAuthorization 和 其他 IIS 7.x 模块(以及 ASP.NET 模块)一起在原生管道里处理即可。
如图,可以在 IIS 7.x 模块配置特性里启用这个配置项:
启用托管模块 UrlAuthorization 后,网站的所有资源都被 ASP.NET 安全保护。文件、图片都成立。现在,重新配置 web.config 文件,使 IIS 授权允许任何用户访问网站,但 ASP.NET 授权显式禁止匿名用户的访问。现在,反问一个图片资源,即使 IIS 7.x 许可了,但 ASP.NET 模块也会拒绝访问。
只有不使用 Windows 验证时把 ASP.NET 的 UrlAuthorization 模块用于原生处理管道才是有意义的。