《Pro ASP.NET MVC 3 Framework》学习笔记之三十四 【验证与授权】

使用windows验证(Using Windows Authentication)

在软件术语里面,验证的意思是测定身份。这个跟授权是完全分开的,授权是让合适的人做合适的事情,授权通常在验证之后发生。ASP.NET的验证功能也紧紧围绕识别访问者的身份并且设置决定实际的访问者能够做什么的安全上下文(security context)。最简单的验证方式就是把这个任务委托给IIS(这通常适合内部系统)。在配置文件里启用Windows身份验证,如果我们使用的Intranet应用程序模版,默认会使用这个配置。如下:
<configuration>
<system.web>
<authentication mode=”Windows” />
</system.web>
</configuration>
使用windows验证,ASP.NET依赖于IIS来验证用户的请求,并且有如下模式:
①匿名验证:允许任何用户访问,IIS7默认启用
②基本验证:需要用户提供一个经过验证的用户名和密码,验证标识作为文本会从浏览器发送到服务器,这种模式仅仅应该在SSL连接的基础上使用
③摘要验证:需要用户名和密码,验证标识会被加密为hash代码发送到服务器。安全性比基本验证更高,并且需要服务器是一个域控制器。
④windows验证:通过windows域透明地建立用户的身份,不需要提供其他的认证。在企业内部局域网使用广泛,不适合面向Internet的应用。
可以在IIS管理器里面设置验证模式,如下:
如果我们想让所有的请求都经过验证,需要我们禁用匿名验证功能。如果只是部分限制,那么可以启用匿名验证并对actions和controllers使用验证过滤器。在局域网部署程序,那么使用windows验证是非常有用的。如果用户是来自Internet,应用程序倾向于依赖表单验证。

使用表单验证(Using Forms Authentication)

表单验证非常适合面向Internet网的程序,当然建立表单验证也会比windows验证更加复杂,一旦设置都完毕,那么比windows验证将会更加灵活。表单验证的安全性依赖于一加密的cookie——.ASPXAUTH。这个cookie内容类似于:
9CC50274C662470986ADD690704BF652F4DFFC3035FC19013726A22F794B3558778B12F799852B2E84
D34D79C0A09DA258000762779AF9FCA3AD4B78661800B4119DD72A8A7000935AAF7E309CD81F28
用FormsAuthentication.Decrypt对其进行解密,可以得到一个FormsAuthenticationTicket对象并且具有如下属性:

《Pro ASP.NET MVC 3 Framework》学习笔记之三十四 【验证与授权】-attach
其中里面一个关键的属性就是Name,这个跟用户请求的相关联的身份标识。系统的安全性的保证来自于cookie数据的机密和使用服务器的machine keys的签名。这些是由IIS自动生成的,并且如果没有这些keys,包含在cookie里的验证信息是不能被读取和修改的。

Tip:当我们把使用了表单验证的程序部署到服务器集群时,必须保证请求总是返回生成cookie的服务器或保证所有的服务器具有相同的machine keys。这些Keys可以使用IIS管理器里面的machine keys选项来生成和配置。

建立表单验证(Setting Up Forms Authentication)

当我们使用互联网应用程序模版创建一个MVC程序时,默认启用了表单身份验证,相关的配置如下:
<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>
这个配置能够适合大多数程序,当然也可以定义更多的属性来控制,这些属性如下:

属性名 默认值 描述
name .ASPXAUTH cookie名
timeout 30分钟 超时时间
slidingExpiration true 滑动过去时间
domain None 跨子域共享(www.example.com和a.example.com)
path / 设置验证cookie发送到指定的URL,这个让我们可以在同一个域中寄宿多个应用程序而不会暴露彼此的验证cookie
loginUrl /login.aspx 如果表单需要用户登录,重定向到指定的登录页面
cookieless UserDeviceProfile 启用无cookie验证
requireSSL false 设置为true会建议浏览器仅仅在使用SSL加密的请求中传递cookie

在web.config文件启用表单验证,当没有经过验证的用户访问到任何标记了[Authorize]的controller和action时会跳转到登录页。

使用无cookie的表单验证(Using Cookieless Forms Authentication)

表单验证支持无cookie模式,这种情况下验证的票据存放在URL里,数据仍然是签名和加密的,但是会作为URL的一部分发送到服务器。只要每一个请求包含了验证数据,那么用户接收到同样应用程序的体验跟启用了cookie是一样的。启用无cookie验证的配置如下:
<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ cookieless=”UseUri”>
</forms>
</authentication>
当用户登录时,他们会被重定向到一个像如下的URL:
/(F(nMD9DiT464AxL7nlQITYUTT05ECNIJ1EGwN4CaAKKze-9ZJq1QTOK0vhXTx0fWRjAJdgSYojOYyhDil
HN4SRb4fgGVcn_fnZU0x55I3_Jes1))/Home/ShowPrivateInformation
仔细观察会发现,URL遵循这样的模式:/(F(authenticationData))/normalUrl
我们不用特别的步骤和配置,路由系统会仔细转换这些URL以至于我们的路由配置能够适应任何改变并且HTML辅助方法会自动生成这种类型的URL。不推荐使用无cookie的验证,因为这种验证非常脆弱,如果有一个链接没有包含验证信息,那么用户马上就被注销了。无cookie验证也是不安全的,任何一个人复制你的URL并共享给其他人,那么第一个用户的session将会被劫持。而且,如果我们依赖了从第三方服务器获取的内容,那么我们的验证数据会通过浏览器的引用头(Referer header)发送给第三方。最后,这个URL看起非常丑陋,没有可读性和吸引力。

使用Membership,Roles和Profiles

在SportsStroe项目里面,我们在Web.config文件里面存储了用户的证书信息,对于小的程序和用户不会经常改变并且用户数量很小的情况下可以这样使用。ASP.NET提供了一套标准的用户账户系统来支持常用的用户账户管理任务,包含注册,密码管理,个性设置。具有三个关键的功能区域:
Membership:注册用户账户并访问账户详情和授权证书
Roles:把用户放入组里面,对身份验证的典型应用
Profiles:存储每一个用户的基础数据
ASP.NET对上面的三个区域提供了标准的实现,但是我们也可以跟自定义的实现混合使用,通过一个providers系统就可以实现。内置的providers能够以不同的方式存储数据,包括SQL Server和活动目录。

建立和使用Membership
ASP.NET里面已经有了SqlMembershipProvider和ActiveDirectoryMembershipProvider两个提供者。下面会介绍如何最常规的使用:

建立SqlMembershipProvider

当使用互联网应用程序模版创建一个MVC程序时,默认配置了SqlMembershipProvider。配置的Web.config如下:
<configuration>
<connectionStrings>
<add name=”ApplicationServices”
connectionString=”data source=.\SQLEXPRESS;Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true”
providerName=”System.Data.SqlClient” />
</connectionStrings>

<system.web>
<membership>
<providers>
<clear/>
<add name=”AspNetSqlMembershipProvider”
type=”System.Web.Security.SqlMembershipProvider”
connectionStringName=”ApplicationServices”
enablePasswordRetrieval=”false”
enablePasswordReset=”true”
requiresQuestionAndAnswer=”false”
requiresUniqueEmail=”false”
maxInvalidPasswordAttempts=”5″
minRequiredPasswordLength=”6″
minRequiredNonalphanumericCharacters=”0″passwordAttemptWindow=”10″
applicationName=”/” />
</providers>
</membership>
</system.web>
</configuration>
SQL Server Express版本的数据库支持一个user实例的数据库,这些数据库是不用在之前配置就可以使用的。付费版的SQL Server不支持用户实例数据库,需要在使用之前准备好该数据库。安装方法:运行aspnet_regsql.exe。安装完了可以去SQL Server management studio里面查看下。

管理Membership

Membership API包含对注册用户的管理方法:添加和移除账户,重置密码等等。对于一些简单的情形,完全可以使用站点管理工具(VS里面有对应的按钮)。如下:
一旦部署了应用程序,我们能够通过IIS .NET用户选项来管理应用程序的用户。使用.NET User选项时,IIS管理工具读取Web.config文件并且视图确保membership提供者是值得信任的。不太走运的是,IIS管理工具是基于.NET2.0并且尚未更新到支持ASP.NET4.当前的IIS版本,微软对使用.NET4的程序禁用了.NET Users选项。下面看看示例的配置文件:
<?xml version=”1.0″ encoding=”UTF-8″?>

<configuration>
<connectionStrings>
<add name=”ApplicationServices” connectionString=”data Source=TITAN\SQLEXPRESS;
Initial Catalog=aspnetdb;
Persist Security Info=True;
User ID=adam;Password=adam”
providerName=”System.Data.SqlClient” />
</connectionStrings>

<system.web>

<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>

<membership>
<providers>
<remove name=”AspNetSqlMembershipProvider”/>
<add name=”AspNetSqlMembershipProvider”
type=”System.Web.Security.SqlMembershipProvider,
System.Web, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a”
connectionStringName=”ApplicationServices”
applicationName=”/”
/>
</providers>
</membership>
</system.web>
</configuration>
这是一个精简过的Web.config文件,只包含两个配置节:connection string和membership database,使用时编辑适合自己的环境。

创建一个自定义的Membership提供者

通过从抽象类MembershipProvider派生可以创建自定义的membership提供者,如下所示:
using System;
using System.Web.Security;
using System.Collections.Generic;

namespace MvcApp.Infrastructure {

public class SiteMember {
public string UserName { get; set; }
public string Password { get; set; }
}

public class CustomMembershipProvider : MembershipProvider {
private static List<SiteMember> Members = new List<SiteMember> {
new SiteMember { UserName = “adam”, Password = “secret” },
new SiteMember { UserName = “steve”, Password = “shhhh” }
};

public override bool ValidateUser(string username, string password) {
return Members.Exists(m => m.UserName == username && m.Password == password);
}

… …
}
}
上面的Provider使用一个静态的用户和密码列表来执行验证,为了简单说明原理,这里忽略了除ValidateUser之外的其他的方法。接着在配置文件注册自定义的Provider:
<configuration>
<system.web>
<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>

<membership defaultProvider=”MyMembershipProvider”>
<providers>
<clear/>
<add name=”MyMembershipProvider”
type=”MvcApp.Infrastructure.CustomMembershipProvider”/>
</providers>
</membership>
</system.web>
</configuration>
下面截取一个在SportsStore里面使用的例子:
using System.Web.Security;
using SportsStore.WebUI.Infrastructure.Abstract;

namespace SportsStore.WebUI.Infrastructure.Concrete {
public class FormsAuthProvider : IAuthProvider {

public bool Authenticate(string username, string password) {

bool result = Membership.ValidateUser(username, password);
if (result) {
FormsAuthentication.SetAuthCookie(username, false);
} return result;
}
}
}
建立并使用角色

前面介绍了验证,还有另外一个常见的安全需求就是授权——决定用户在验证之后能够做什么。ASP.NET使用基于角色的授权机制,这意味着actions是限制在角色里面的,属于角色的用户能够执行相应的action方法。角色通过唯一的字符串值来表示,例如可以定义如下三个角色:ApprovedMember CommentsModerator SiteAdministrator。每一个角色都是完全独立的,没有层级关系。ASP.NET平台期望我们通过provider模型来使用角色,提供了常用的API。当然也可以自定义:

建立SqlRoleProvider

SqlRoleProvider类是对SqlMembershipProvider的补足,使用了同样的数据库。使用互联网应用程序模版创建MVC程序,VS会自动添加相应的元素来建立SqlRoleProvider,如下:

<configuration>
<system.web>
<roleManager enabled=”false”>
<providers>
<clear/>
<add name=”AspNetSqlRoleProvider”
type=”System.Web.Security.SqlRoleProvider”
connectionStringName=”ApplicationServices”
applicationName=”/” />
<add name=”AspNetWindowsTokenRoleProvider”
type=”System.Web.Security.WindowsTokenRoleProvider”
applicationName=”/” />
</providers>
</roleManager>
</configuration>

两个Role Provider注册了,默认都没有启用。要建立SqlRoleProvider必须修改roleManager元素
如:<roleManager enabled=”true” defaultProvider=”AspNetSqlRoleProvider”>

管理Roles

可以使用管理members的方法管理roles,可以使用VS里面的ASP.NET图形化的配置工具来操作。下面介绍创建自定义的Roles Provider:

using System;
using System.Web.Security;

namespace MvcApp.Infrastructure {

public class CustomRoleProvider : RoleProvider {

public override string[] GetRolesForUser(string username) {

if (username == “adam”) {
return new string[] { “CommentsModerator”, “SiteAdministrator” };
} else if (username == “steve”) {
return new string[] { “ApprovedUser”, “CommentsModerator” };
} else {
return new string[] { };
}
}

……
}
}

通过从RoleProvider派生来定义自己的提供者,我只需要实现GetRolesForUser方法就可以使用了。同样,创建以后需要注册:

<configuration>
<system.web>

<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>

<membership defaultProvider=”MyMembershipProvider”>
<providers>
<clear/>
<add name=”MyMembershipProvider”
type=”MvcApp.Infrastructure.CustomMembershipProvider”/>
</providers>
</membership>

<roleManager enabled=”true” defaultProvider=”MyRoleProvider”> <providers>
<clear/>
<add name=”MyRoleProvider”
type=”MvcApp.Infrastructure.CustomRoleProvider”/>
</providers>
</roleManager>

</system.web>
</configuration>

建立并使用Profiles

Membership记录我们的用户,roles记录允许用户做什么操作。如果我们想记录用户个性化的一些数据,如会员积分等等信息。那么通常可以使用Profiles,这对于使用SqlMembershipProvider的小应用程序来说是一个非常有吸引力的功能。

建立SqlProfileProvider

使用互联网应用程序模版创建一个新的MVC程序,包含在Web.config里面的元素创建SqlProfileProvider。如下:

<configuration>
<system.web>
<profile>
<providers>
<clear/>
<add name=”AspNetSqlProfileProvider”
type=”System.Web.Profile.SqlProfileProvider”
connectionStringName=”ApplicationServices”
applicationName=”/” />
</providers>
</profile>
</system.web>
</configuration>

配置/读/写Profile数据

在使用Profile之前,我们必须定义Profile的数据结构,可以在Web.config的profile节里面添加属性元素如下:

<profile>
<providers>
<clear/>
<add name=”AspNetSqlProfileProvider”
type=”System.Web.Profile.SqlProfileProvider”
connectionStringName=”ApplicationServices”
applicationName=”/” />
</providers>
<properties>
<add name=”Name” type=”String”/>
<group name=”Address”>
<add name=”Street” type=”String”/>
<add name=”City” type=”String”/>
<add name=”ZipCode” type=”String”/>
<add name=”State” type=”String”/>
</group>
</properties>
</profile>

上面定义的属性都是String类型的,但是Profile支持任何能够序列化的.NET类型。在我们使用Web Forms的时候,通过跟profiles属性一致的代理对象来访问profile数据。这个在MVC里是不可行的,但是我们可以使用HttpContext.Profile属性来访问。如:

public ActionResult Index() {

ViewBag.Name = HttpContext.Profile[“Name”];
ViewBag.City = HttpContext.Profile.GetProfileGroup(“Address”)[“City”];

return View();
}
[HttpPost]
public ViewResult Index(string name, string city) {

HttpContext.Profile[“Name”] = name;
HttpContext.Profile.GetProfileGroup(“Address”)[“City”] = city;

return View();
}

ASP.NET框架在我们第一次访问profile数据时,使用profile提供者载入用户的profile属性并且在请求结束时通过profile提供者写回,我们不用显示的保存,这个是自动完成的。

启用匿名的Profiles

默认情况下,profile数据只对经过验证的用户可用,没有登录的用户访问时会抛异常。可以通过启用对你们profile的支持来解决:

<configuration>
<system.web>
<anonymousIdentification enabled=”true”/>
<profile>
<providers>
<clear/>
<add name=”AspNetSqlProfileProvider”
type=”System.Web.Profile.SqlProfileProvider”
connectionStringName=”ApplicationServices”
applicationName=”/” />
</providers>
<properties>
<add name=”Name” type=”String” allowAnonymous=”true”/>
<group name=”Address”>
<add name=”Street” type=”String”/>
<add name=”City” type=”String” allowAnonymous=”true”/>
<add name=”ZipCode” type=”String”/>
<add name=”State” type=”String”/>
</group>
</properties>
</profile>
</system.web>
</configuration>

当匿名的身份标识启用时,ASP.NET框架将会通过一个.ASPXANONYMOUS的cookie来记录你们用户,并且cookie的过期时间是10000分钟(70天左右)。启用了匿名profile以后,没有验证的用户也可以读写profile数据,没一个没有验证的用户会自动为他们创建一个账户并保存在profile数据库里面。

创建自定义的Profile提供者(Profile Provider)

通过从ProfileProvider派生来创建自定义的profile provider。如下:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web.Profile;

namespace MvcApp.Infrastructure {
public class CustomProfileProvider : ProfileProvider {
private IDictionary<string, IDictionary<string, object>> data =
new Dictionary<string, IDictionary<string, object>>();

public override SettingsPropertyValueCollection GetPropertyValues(
SettingsContext context, SettingsPropertyCollection collection) {

SettingsPropertyValueCollection result = new SettingsPropertyValueCollection();

IDictionary<string, object> userData;
bool userDataExists
= data.TryGetValue((string)context[“UserName”], out userData);

foreach (SettingsProperty prop in collection) {
SettingsPropertyValue spv = new SettingsPropertyValue(prop);
if (userDataExists) {
spv.PropertyValue = userData[prop.Name];
}
result.Add(spv);
}
return result;
}

public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection) {

string userName = (string)context[“UserName”];
if (!string.IsNullOrEmpty(userName)) {
data[userName] = collection
.Cast<SettingsPropertyValue>()
.ToDictionary(x => x.Name, x => x.PropertyValue); }
}

……
}
}

上面的provider非常简单,仅仅将数据存储在内存。接着需要注册:

<configuration>
<connectionStrings>
<add name=”ApplicationServices”
connectionString=”data Source=TITAN\SQLEXPRESS;
Initial Catalog=aspnetdb;
Persist Security Info=True;
User ID=adam;Password=adam”
providerName=”System.Data.SqlClient” />
</connectionStrings>

<system.web>
<authentication mode=”Forms”>
<forms loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>

<membership defaultProvider=”MyMembershipProvider”>
<providers>
<clear/>
<add name=”MyMembershipProvider”
type=”MvcApp.Infrastructure.CustomMembershipProvider”/>
</providers>
</membership>

<roleManager enabled=”true” defaultProvider=”MyRoleProvider”>
<providers>
<clear/>
<add name=”MyRoleProvider”
type=”MvcApp.Infrastructure.CustomRoleProvider”/>
</providers>
</roleManager>

<profile enabled=”true” defaultProvider=”MyProfileProvider”>
<providers>
<clear/>
<add name=”MyProfileProvider”
type=”MvcApp.Infrastructure.CustomProfileProvider”
/>
</providers>
<properties>
<add name=”Name” type=”String”/>
<group name=”Address”>
<add name=”Street” type=”String”/>
<add name=”City” type=”String”/>
<add name=”ZipCode” type=”String”/>
<add name=”State” type=”String”/>
</group>
</properties>
</profile>
</system.web>
</configuration>

为什么不应该使用基于URL的授权(Why You Shouldn’t Use URL-Based Authorization)

ASP.NET曾经依靠匹配URL到应用程序的目录结构来实现授权,这样的做法非常多以至于在URL模式中定义授权规则有非常重要的意义。很多Webform的程序,例如把所有管理员的aspx页面放在一个Admin的文件夹下,这意味着我们可以使用基于URL的授权来限制登录的用户对/Admin/*的访问。这种方式对MVC框架是没有用的,因为路由系统的存在打破了URL跟文件系统之间的联系。正确做法是在controller上使用Authorize过滤器(前面的章节有介绍)

使用IP地址和域名限制访问(Restricting Access Using IP Addresses and Domains)

首先介绍下这么做的风险:
这种方式应该谨慎使用,有很多潜在的风险。首先这是基于URL的授权,并且不能很完善的跟MVC的路由系统相适应。其次,用这种方式限制访问没有考虑用户怎样跟我们应用程序的可能的交互。举一个例子,亚当今天早上在家第一次上网,后来他在咖啡店连接上网,在朋友家上网,在另一个朋友的办公室上网,最后回到家里上网。在上面的情形里有些是通过VPN连接的,有些是直接连的网。基于用户从哪连接网络来限制访问很容易建立,但是要想得到正确的判断很困难。下面的Web.config使用了IP地址和域名限制来建立:

<configuration>
<location path=”Home”>
<system.webServer>
<security>
<ipSecurity enableReverseDns=”true”>
<clear/>
<add ipAddress=”192.188.100.1″/>
<add ipAddress=”169.254.0.0″ subnetMask=”255.255.0.0″/>
<add domainName=”mydomain.com”/>
<remove domainName=”otherdomain.com”/>
</ipSecurity>
</security>
</system.webServer>
</location>
</configuration>

我们在ipSecurity元素里面定义策略,可以使用IP地址和域名来限制访问,但是需要显示的启用enableReverseDns=true。建议在启用域名支持时需要谨慎,因为它需要为每一个请求反向查找DNS,这是非常耗时的并且会严重限制服务器的吞吐量。使用clear元素移除任何存在的策略,然后使用add和remove定义我们的新策略。add添加新的限制,如:
<add ipAddress=”192.188.100.1″/> 这样阻止处理来自该IP地址的请求,再如:<remove domainName=”otherdomain.com”/> ,这样就允许来自该域名的请求。

好了,本章的笔记就到这里,还剩下最后一章的笔记,谢谢大家持续支持。希望路过的朋友留下你们的各种建议或意见:-)

作者:张雪飞
出处:https://zhangxuefei.site/p/134
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

发表评论

电子邮件地址不会被公开。 必填项已用*标注