在上一篇文章中,我们已经使用WIF构建了一个基于MVC4的简单的身份验证程序,在这篇文章里,我们将探讨一下到底什么是WIF,以及它的工作原理。然后在下一篇文章开始,我们将实际操作,实现单点登录功能。
身份标识的挑战
在上一篇文章也提及到了,大部分的开发人员并不是安全方面的专家,很多人对于身份验证,授权以及用户体验个性化等工作感觉非常的不爽。传统的计算机技术的课程里通常也不会教这些课题,因此这些东西经常在软件开发周期的后半部分才会凸显出来。当今,一个公司里有数十上百个Web应用程序和服务已不是什么新鲜事儿,很多公司都有自己的一套用户标识,而且大部分公司都不会去用特定的身份验证方法。开发人员都知道要为每一个程序都支持身份标识是多么乏味的事情,而且IT专家也知道要管理这些程序的结果集是多么昂贵。解决此问题的很有用的一步就是将用户账号集中到企业目录中去。通常只有IT专家才知道查询目录的最有效方法,但如今这项工作通常都交给了开发人员。而且面对合并,收购和合作伙伴关系的时候,开发人员可能要访问多个目录,使用多个API。在微软 .NET Framework 里,有很多在程序里支持身份标识的不同方法,而且每一个通信框架对身份标识都有不同的处理,它们采用了不同的对象模型,不同的存储模型,等等。甚至在ASP.NET里,开发人员对于上哪找身份标识也会感到困惑:应该用 HttpContext.User 属性?还是 Thread.CurrentPrincipal?对密码的使用不当导致了网络钓鱼的猖狂。而这么多程序都自己干自己的事儿,对公司来说就很难升级到强大的身份验证技术。
更好的解决方案
一个解决这些问题的方法是别再在每个新的程序里建造自定义的身份标识管道和用户账户数据库。但即使是依赖企业目录的开发人员仍然对合并,收购和合作伙伴感到痛苦,甚至还有可能由于其它程序的低效查询拖累了目录而被责难为性能低下。本文所要讲的基于声明的解决方案不需要开发人员为了查询用户身份标识的细节而连到任何特定的企业目录。相反,用户请求带着程序需要干活儿的标识信息一同到达。带着这些声明的用户请求到达的时候,用户就已经是验证通过了,应用程序无需担心管理或者查找用户账户的事儿,只需专注于业务即可。在应用程序之外处理身份验证可以为开发人员,IT专家和用户带来很多好处。简单地说,需要每个人来管理的用户账户少了,身份验证的集中化使其随着其发展更容易升级到强壮的身份验证方法,即使与其它平台和组织进行联合身份标识也是如此。本文将帮你,作为一个开发人员,去理解基于声明的标识模型以及通过使用微软的新框架Windows Identity Foundation(WIF)来利用它。
什么是Windows Identity Foundation?
Windows Identity Foundation (WIF) 是一组 .NET Framework 类。它是为了在程序里实现基于声明的标识的框架。通过使用它,将更简单地感受到本文所说的基于声明的标识模型的好处。 Windows Identity Foundation可以用于任何基于.Net Framework 3.5 SP1以上版本的Web应用程序或者Web服务。WIF只是微软标识和访问平台(Identity and Access Platform)软件家族的一部分,这个家族实现了可互操作的标识原系统(Identity MetaSystem)这么一个共享的产业愿景。包括活动目录联合服务 (ADFS) 2.0 (之前被称为 “Geneva” Server), Windows CardSpace 2.0,以及 Windows Identity Foundation (之前称为 “Geneva” Framework),来自于微软新的基于声明访问策略的核心部分。可以参考Identity Management in Active Directory来获取更多关于ADFS和CardSpace组件的信息。
基于声明的标识模型
当创建声明感知(claims-aware)的应用程序时,用户向程序出示他的身份标识,标识以一组声明来表示(见图1)。一个声明可能是用户名,另一个声明可能是电子邮件地址。此处的想法是一个外部的标识系统被配置来对于用户发起的每一个请求,都为程序提供它所需要知道的用户信息,以及接收到的标识数据都经过受信任来源的加密保护。
图1 用户出示声明
基于此模型,单点登录更容易了,而且应用程序也不用再干这些事儿了:
- 验证用户身份
- 储存用户账户和密码
- 调用企业目录来查询用户标识明细
- 从其它平台或其它公司集成标识系统
上面说了这么多,可能已经出现了很多陌生的名词,接下来我们以一个实际生活中的例子来解释一下这些名词吧。
假设你要去电影院看一部纪录片,要考虑以下因素:
1、该纪录片含有不适合未成年人观看的情节,因此电影院的员工需要你出示身份证明来验证你是否适合观看。你掏出钱包,却发现你的身份证丢了,而驾照也已经过期了。
2、你决定不看首映,先到附近的DOL(Department of Licensing,牌照部)领取一个新的驾照。
3、工作人员先看你是否和记录里的照片长得一致,或许还会让你去视力表那测一下。当他确信了你确实是你之后,就会给你发新的驾照。
4、你回到电影院出示了新驾照,工作人员确认了你确实够年龄观看影片了,就会给你下一场电影的门票。
上图演示了这个例子的过程。
上面这个例子应该说是很常见的了。那么接下来我们把它抽象到我们的身份验证里去看看:
假设我们的系统包括了一个用户(主体,subject)和他要访问的程序。在上面的例子里,这个用户就是要去看电影的人;通常,subject可以是任何东西,无论是真实的用户还是无人值守的程序标识。应用程序可以是一个网站,Web服务,或者通常的任何需要身份验证和授权用户的软件。用标识里的行话来讲,就是叫做依赖方(relying party,RP)。在上面的例子里,RP就是电影院及其工作人员。系统里可能包括一个到多个标识提供者(identity provider,IP)。IP是认识subject的一个实体。它知道该如何去验证subject,就像上面的例子,DOL知道如何根据档案库里的照片和顾客本人的脸去做做比较;它知道顾客的事儿,就像DOL知道它区域里每个司机的出生日期。IP是一个抽象的角色,但它需要具体的组件:目录,用户系统信息库,以及身份验证系统,这些都是IP用来履行其职责的部分例子。我们假设subject有都多种标准途径来用IP进行身份验证以及接收返回的必要用户信息(如上例的出生日期)。我们就将这些用户信息称为声明(claims)。
声明这个词终于千呼万唤始出来了。声明是一个实体对subject的陈述。这个陈述可以是与subject关联的任何东西,无论是诸如出生日期这样的属性还是这个subject属于某个特定的安全组。声明和简单的属性不一样的地方是声明通常是和发行它的实体相关联的。这是一个很重要的区别:它提供了一个标准,让你自己决定是否去信任这个subject。回想一下上面的例子,印在驾照上的日期与随便在某张边条纸上写得很潦草的日期,电影院员工信任的是前者而不是后者。
上图描述了抽象后的过程,接下来对这个过程详细解释一下:
1、主体(subject)想要通过某种途径(浏览器,富客户端等等)去访问RP应用程序。主体先去了解RP的策略。得知RP信任哪个标识,需要哪种声明,以及要用哪个安全协议。
2、主体选择其中一个RP信任的IP,并检测其策略,得知要用什么安全协议。然后发送请求到IP并发行一个能匹配RP需求的令牌。这个过程和去DOL请求一个含有出生日期的档案是一样的。主体需要提供一些证明来让IP进行辨识。要使用的协议详情在IP的策略里进行描述。
3、IP 处理该请求;如果请求合乎要求,它去获取需要的声明值,以安全令牌的形式发送回给主体。
4、主体从IP那接收到安全令牌,并将它连同最开始的请求一起发送给RP程序。
5、RP 程序检查传入的令牌并验证它是否符合所有的要求(是否从受信任的IP来的,格式是否正确,是否被篡改过,是否包含正确的声明集,等等)。如果所有东西都合乎要求,RP就准许主体进行访问。
好,了解了基于声明的身份标识基本流程之后,我们再来看看WIF是如何工作的。
上图演示了WIF为ASP.NET程序处理身份验证的简要过程。
1、WIF坐镇应用程序的前端,位于ASP.NET管线中,当未验证通过的用户请求页面时,WIF将浏览器跳转到标识提供(IP)页面。
2、这里IP对用户进行身份验证,不管用什么方法(可能显示一个用户民和密码的页面,可能用Kerberos,或者其它方法)。然后生成一个令牌,与所需的声明一起发送回去。
3、浏览器将从IP那里得到的令牌post给应用程序,WIF再次截获该请求。
4、如果令牌满足应用程序的要求(也就是,来自于正确的IP,包含正确的声明,等等),用户就被认为是验证通过了。WIF然后放置一个cookie,并建立起一个会话。
5、传入的令牌里的声明在程序里用代码就可以对其进行使用了,此时控制权将交给应用程序。
只要会话cookie有效,随后的请求就不需要再走一遍这个流程,因为用户已经被认为是通过了身份验证了。
WIF用到的协议时WS-Federation协议,主要有两个HTTP模块来处理这些事情:WSFederationAuthenticationModule(WSFAM)和 SessionAuthenticationModule 。
在应用程序中使用WIF归结为以下三点:
1、配置应用程序以使WIF的HTTP模块坐镇ASP.NET管线的前端。
2、配置WIF模块以使它们引用目标IP,使用正确的协议,保护应用程序的计划资源,以及执行所需的应用程序策略。
3、在需要应用程序逻辑的时候,从应用程序里访问声明的值,在需要用户标识属性的场合对其进行处理。
IClaimsIdentity 和 IClaimsPrincipal
WIF提供了两个IIdentity和IPricipal的扩展:分别是 IClaimsIdentity 和 IClaimsPrincipal ,它们是用来在WIF管线里处理声明的。它们的实例存在于ASP.NET程序里的HttpContext.Current.User属性中。你可以像平常的IIdentity和IPrincipal编程模型那样来使用它们,或者把它们转换成正确的实例并利用其新的功能。
IClaimsIdentity定义如下:
- public interface IClaimsPrincipal : IPrincipal
- {
- // ...
- // Properties
- ClaimsIdentityCollection Identities { get; }
- }
public interface IClaimsPrincipal : IPrincipal
{
// ...
// Properties
ClaimsIdentityCollection Identities { get; }
}
由于IClaimsPrincipal是IPrincipal的一个扩展,所有的通常功能(如IsInRole)都是支持的。
唯一值得注意的地方是Identities集合,实际上是一组IClaimsIdentity的值。来看看IClaimsIdentity的定义:
- public interface IClaimsIdentity : IIdentity
- {
- // ...
- ClaimCollection Claims { get; }
- }
public interface IClaimsIdentity : IIdentity
{
// ...
ClaimCollection Claims { get; }
}
这里我省去了这个接口的大部分成员,只留了这个最重要的,也就是与当前用户相关的一组声明。那么声明是长什么样的呢:
- public class Claim
- {
- // ...
- // Properties
- public virtual string ClaimType { get; }
- public virtual string Issuer { get; }
- public virtual IClaimsIdentity Subject { get; }
- public virtual string Value { get; }
- }
public class Claim
{
// ...
// Properties
public virtual string ClaimType { get; }
public virtual string Issuer { get; }
public virtual IClaimsIdentity Subject { get; }
public virtual string Value { get; }
}
我这里再次省去了其它成员。列出来的这些属性基本看名字就知道是干什么用的了:
■ ClaimType 表示声明的类型:出生日期,角色,和组成员资格,这些都是很好的例子。WIF带有很多代表声明类型名称的常量;然而,如果需要的话,你可以很容易地定义自己的类型。典型的声明类型是以URI来表示。 ■ Value 很明显了,就是指定声明的值。虽然可以用其它CLR类型来表示,但通常都是一个字符串。(比如出生日期)
■ Issuer 指示发行当前声明的IP名称。
■ Subject 表示当前的Claim属于哪个 IClaimsIdentity,表示声明引用的是哪个主体的标识。
我们来举一个很简单的例子。假如你的程序已经配置好了WIF来使用基于声明的身份标识。身份验证在会话的刚一开始就发生了,所以在执行过程中你都可以假设用户已经通过了身份验证。在你代码里的某个特定点上,你需要向你的用户发送一封e-mail。因此你需要获取她的e-mail地址。在WIF里就可以这么写:
- IClaimsIdentity identity = Thread.CurrentPrincipal.Identity as IClaimsIdentity;
- string Email = (from c in identity.Claims
- where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.Email
- select c.Value).SingleOrDefault();
IClaimsIdentity identity = Thread.CurrentPrincipal.Identity as IClaimsIdentity;
string Email = (from c in identity.Claims
where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.Email
select c.Value).SingleOrDefault();
第一行代码从当前线程里获取当前的IClaimsIdentity,第二行代码用LINQ从当前声明集合里获取e-mail地址。查询条件很直观:查询所有的声明,看看哪个类型是Email声明类型,并返回第一个找到的结果。上面演示的代码没有指示验证用户用的是哪种协议或者证书类型。这就意味着你可以随便在验证身份的代码里做任何修改,而这里的代码不需要做任何改变。
总结
我们这次讲了基于声明的身份标识以及WIF的基本工作原理,在接下来的文章中,我们将利用WIF的这些美妙特性,来构建我们的单点登录实现。