适用于:
Microsoft® .NET Framework
Web Services Enhancements 2.0 for Microsoft® .NET
WS-Policy 规范
摘要:介绍如何使用 Web Services Enhancements 2.0 for Microsoft .NET (WSE 2.0) 来集成基于 X.509 的 WS-Security 验证和 Microsoft .NET Framework 中基于角色的安全功能。并着重介绍如何使用 WSE 2.0 中的 WS-Policy 来极大地简化任务。
从 Microsoft Download Center 下载相关的示例代码(英文)。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)
简介
签名邮件的快捷路由
使用声明式和命令式基于角色的安全性
实现 Identity 和 Principal
将所有功能组合起来
只需一行代码
Microsoft .NET Framework 和 Microsoft ASP.NET 提供了许多保护代码安全性的功能。如果仅仅使用与 HttpContext.Current.User.IsInRole() 类似的结构就能保护对基于 WSE 的 Web 服务方法的访问,那不是很好吗?本文向您介绍如何将 WSE 2.0 中的邮件签名和验证功能同 .NET Framework 中基于角色的权限机制相结合。
在常规的 Web 应用程序或 Web 服务中,您可以只借助于 IIS (SSL) 的验证和加密手段。在这种情况下,可以这样配置目录:要求用户使用基本的 HTTP 安全设置或 Windows 集成的安全设置通过 HTTP 协议发送登录凭据。
刚开始时,使用 HTTP 来验证 Web 服务请求似乎是一个不错的方法,但是随着 WS-Routing 的出现,情况就发生了本质的变化:在邮件的发件人和最终的收件人之间不再存在直接的 HTTP 连接,但是这条路由路径上可能会使用各种不同的协议。这使传输层的所有安全手段都成为一个纯粹的可选附加物,但却不能保证消息的端对端完整性和安全性。
为 Web 服务提供全面的端对端服务的一种方法是,按照 WS-Security 规范使用 X.509 证书对待发邮件进行签名。
可以通过使用某个著名的证书颁发机构 (CA)(如 Verisign)来获得 X.509 证书,或者就使用 Windows Certificate Services 创建您自己的 CA。安装了这种作为 Windows 2000 Server 或 Windows Server 2003 的可选组件的服务后,可以将浏览器指向 http://<servername>/certsrv 来请求创建新的证书。在您的请求被系统管理员授权后,可以使用 http://<servername>/certsrv 中的同一个 Web 应用程序将新创建的证书添加到您的私有证书存储中。还要确保在导入证书时,通过手动选择存储位置(包括“物理位置”)将证书颁发机构的根证书添加到您的 Web 服务器“本地计算机”存储中。
创建并获得 X.509 证书后,就可以使用它来对您的 Web 服务请求进行签名。但是请稍候,您必须首先使您的项目能够支持 Web Services Enhancements 2.0。以 .NET Developer(.NET 开发人员)模式安装 WSE 2.0 之后,右击 Visual Studio® .NET 内的任何项目,并选择 WSE Settings 2.0(WSE 设置 2.0)打开如图 1 所示的对话框。
图 1:启用 Web Services Enhancements 2.0
选中对应的复选框后,就会发生很多有意思的事件。首先,指向 Microsoft.Web.Services.dll 的引用将自动添加到您的项目中。接下来,甚至也是更重要的,Add Web Reference ...(添加 Web 引用)和 Update Web Reference(更新 Web 引用)命令的行为将发生更改,允许您以后访问附加的上下文属性。这使您可以变更安全令牌。该更改是通过为每个 Web 引用创建第二个代理来完成的。新生成的代理的名称将被加上后缀“Wse”(例如,代理“MyService”的名称为“MyServiceWse”),并且 Framework 将为其选择一个不同的基类:与非 WSE 代理继承 System.Web.Services.Protocols.SoapHttpClientProtocol 不同,WSE 代理将扩展包含大量附加属性的 Microsoft.Web.Services.WebServicesClientProtocol。
使用这个新基类,现在可以访问 WSE 的 SoapContext,而且能够为待发邮件添加 WS-Security 令牌并进行签名。为了进一步说明基于角色的安全扩展功能,创建了一个演示服务,如下所示:
using System; using System.Web.Services; using System.Security.Permissions; [WebService(Namespace=" http://schemas.ingorammer.com/wse/role-based-security-extensions/2003-08-10")] public class DemoService: System.Web.Services.WebService { [WebMethod] public string HelloWorld() { return "Hello World"; } }
然后向该项目添加了一个 Web 引用,指定它的 URL Behavior 为“dynamic”,并且创建了以下 app.config 文件来指定服务器的位置和客户应该使用的证书。如果您在您的计算机上按照本例进行操作,请注意必须指定您的私有证书存储中包含的一个证书名称!
<configuration> <appSettings> <add key="RoleBasedSecurityClient.demosvc.DemoService"
value="http://server/WSEDemo/DemoService.asmx"/><add key="CertificateName" value="[email protected]"/>
</appSettings> </configuration>
Web Services Enhancements 2.0 现在可以用于创建基于 X.509 证书的二进制安全令牌。这个令牌将被附加在待发邮件上,并将用于对邮件进行加密签名。在以下客户端代码中,增加的功能包括访问用户私有证书存储和查找上述配置文件中指定的证书。
public static void Main(string[] args) { String sto = X509CertificateStore.MyStore; // 打开证书存储X509CertificateStore store = X509CertificateStore.CurrentUserStore(sto);
store.OpenRead(); // 查找要使用的证书String certname = System.Configuration.ConfigurationSettings.AppSettings["CertificateName"];
X509CertificateCollection certcoll = store.FindCertificateBySubjectString(certname);
if (certcoll.Count == 0) { Console.WriteLine("Certificate not found"); } else { X509Certificate cert = certcoll[0]; DemoServiceWse svc = new DemoServiceWse(); SoapContext ctx = svc.RequestSoapContext; // 使用该证书进行邮件签名SecurityToken tok = new X509SecurityToken(cert);
ctx.Security.Tokens.Add(tok);
ctx.Security.Elements.Add(new Signature(tok));
// 调用 Web 服务 String res = svc.HelloWorld(); } Console.WriteLine("Done"); Console.ReadLine(); }
好了!签名邮件的快捷路由就介绍到这里。如果要了解该路径的详细信息,请参阅 Matt Powell 的文章“ WS-Security Authentication and Digital Signatures with Web Services Enhancements(英文)。
好极了 - 邮件已签名。现在做什么?您可以一边访问安全令牌和它在服务器端 [WebMethod] 的证书,一边检查它是否与调用服务时所需的证书相匹配。尽管这在刚开始时似乎很实用,但是必须考虑到您的应用程序需要支持大量的用户,而每个用户都将具有自己的证书。如果您的 Web 服务结构比上述简单示例复杂,很可能还具有不同角色的用户。每个角色将具有不同的权限,以便有些方法只可以被属于特定角色的用户调用。
当实现传统的基于 HTTP 的 Web 应用程序时,您可以通过只访问 HttpContext
来检查 Windows 用户组或 Active Directory 组的成员身份,如下所示:
[WebMethod]
public void AuthorizeOrder(long orderID)
{
if (! HttpContext.Current.User.IsInRole(@"DOMAIN\Accounting")
)
throw new Exception("Only members of 'Accounting' may call this method.");
// ... 更新数据库
}
该语法也允许您检查更小的权限组,例如,依赖于一些参数值的权限组。在下面的示例中,“HR”角色的用户可以调用该方法,但只有“PointyHairedBoss”角色的成员才可以设置高于某一具体数额的工资值:
public void SetMonthlySalary(long employeeID, double salary) { if (! HttpContext.Current.User.IsInRole(@"DOMAIN\HR")
) throw new Exception("Only members of 'HR' may call this method."); if (salary > 2000) { if (! HttpContext.Current.User.IsInRole(@"DOMAIN\PointyHairedBoss")
) throw new Exception("Only the pointy haired boss might set salaries larger than 2K"); } // ... 更新数据库 }
上述代码依赖于一个标准的 ASP.NET 功能 - 虽然只是一个,但是它对于我们的方案来说是较为严重的缺陷:实际上,它不能满足使用端对端验证方案的要求,因为它依赖于 HTTP 验证。第二点是用户的角色成员身份是由查看其 Windows 或 Active Directory 组成员身份决定的。这将导致每当应用程序需要细分的权限级别时,都需要大量的 Windows 组,这种事会让系统管理员感到头疼。
幸运的是,WSE 2.0 提供了必要的扩展性挂钩来供您实现基于角色的安全性。对应的操作更简单:只需向运行时提供必要信息来确定用户的角色成员身份。不然,Framework 怎么会知道属于 PointyHairedBoss 角色的某人已经发出了签名为“[email protected]”的邮件呢?
在这种情况下,要启用基于角色的安全性,可以编写一个从 Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager 派生的自定义安全令牌管理器。将在后面展示这个新的令牌管理器,它将读取一个与下面类似的配置文件来确定证书与角色之间的映射:
<?xml version="1.0" encoding="utf-8" ?> <CertificateMapping> <Certificates> <!-- [email protected] --> <CertificateMap Hash="f5 06 ba 1d 76 3b 59 1f ac 0c 3d ff e8 52 a3 41 44 b5 ed b1"> <Roles> <Role>PointyHairedBoss</Role> <Role>Accounting</Role> <Role>HR</Role> </Roles> </CertificateMap> <!-- [email protected] --> <CertificateMap Hash="d7 fd 06 0d 43 7f 8f bb df a2 ee 9a 55 e4 c4 49 93 65 99 e4"> <Roles> <Role>Accounting</Role> <Role>HR</Role> </Roles> </CertificateMap> </Certificates> </CertificateMapping>
在该配置文件中,每个要指定角色的证书对应一个 <CertificateMap> 项。证书由特色哈希代码来标识,可以使用 Windows Certificate Services 管理工具来获取哈希代码,该管理工具位于通过单击“开始”>Administrative Tools(管理工具)>Certification Authority(证书颁发机构)打开的证书颁发机构 (CA) 中。当运行该工具且选择一个先前颁发的证书时,将看到如图 2 所示的窗口,允许您将哈希代码复制到剪贴板。
通过打开 Internet Explorer 并单击“工具”>“Internet 选项”>“内容”>“证书”,也可以访问当前用户的证书存储中全部证书的此类信息。
图 2:证书详细资料
然后可以使用哈希代码在配置文件中创建一个新的 <CertificateMap> 项,来指定给定证书的关联角色。该文件稍后将被 XmlSerializer 读取,并基于以下两个类进行还原序列化并传送到对象:
public class CertificateMapping { [XmlArrayItem(typeof(CertificateMap))] public ArrayList Certificates = new ArrayList(); public CertificateMap this[String hash] { get { foreach (CertificateMap cert in Certificates) { if (cert.CertificateHash.Replace(" ","").ToUpper() == hash.ToUpper()) return cert; } return null; } } } public class CertificateMap { [XmlAttribute("Hash")] public String CertificateHash; [XmlArrayItem("Role", typeof(String))] public ArrayList Roles = new ArrayList(); }
令牌管理器本身必须继承 Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager。用 Framework 注册这个类后,每个包含在请求邮件中的 X509 二进制安全令牌在通过这个新类的 AuthenticateToken() 方法的验证后才到达 [WebMethod]。因此,该方法使您可以为每个令牌创建一个匹配的 IPrincipal 对象,该对象用于挂钩回到 .NET 安全模型。
要实现安全令牌管理器,首先必须为所需的名称空间添加引用:
using System; using System.Collections; using Microsoft.Web.Services.Security; using Microsoft.Web.Services; using Microsoft.Web.Services.Security.Tokens; using System.Security.Principal; using System.Threading; using System.Xml.Serialization; using System.IO; using System.Web; using System.Security.Permissions;
然后覆盖 AuthenticateToken() 方法以访问传入的安全令牌并为其附加一个匹配的 IPrincipal。
namespace RoleBasedSecurityExtension { [SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.UnmanagedCode)] public class X509RoleBasedSecurityTokenManager: X509SecurityTokenManager { protected override voidAuthenticateToken
(X509SecurityToken token) { base.AuthenticateToken(token);token.Principal = new CertificatePrincipal(token.Certificate);
} } }
.NET Framework 基于角色的安全功能是建立在将 Principal 和 Identity 对象指定到请求上下文基础之上。Principal 对象包含当前用户所属的角色,Identity 对象存储用户本身及其身份验证方面的信息。
这两个接口能在 System.Security.Principal 中找到,它们的内容如下所示:
public interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string role); } public interface IIdentity { string AuthenticationType { get; } bool IsAuthenticated { get; } string Name { get; } }
要完成基于角色的安全性扩展,本文实现了一个自定义的 Principal 对象 (CertificatePrincipal) 和一个自定义的的 Identity 对象 (CertificateIdentity),用来访问当前用户的角色成员身份和访问用于验证的证书。
只要当一封传入邮件到达服务器,安全令牌管理器就会调用该类的 public 构造函数,并以 X509Certificate 对象作为参数。如果角色映射已经被更改,该类将从 XML 文件加载角色映射;或者使用 static 变量中存储的缓存文件。然后,过滤器在从 XML 文件加载的信息中查找给定的证书。检查是否已经记录了给定证书的角色成员身份信息。如果没有,会抛出一个异常;否则就创建一个相应的 Identity 对象。
要实现 Principal 对象,首先必须包含必要的名称空间:
using System; using System.Xml.Serialization; using System.IO; using System.Security.Principal; using Microsoft.Web.Services.Security.X509;
如果需要,public 构造函数会读取 XML 文件并检查是否已经在 certmap.config 中配置了发件人的证书。如果已经配置,将创建一个新的 CertificateIdentity 对象,否则就抛出一个异常。
public class CertificatePrincipal: IPrincipal
{
private static CertificateMapping _map;
private static DateTime _certmapDateTime = DateTime.MinValue;
private CertificateMap _certmap;
private CertificateIdentity _ident;
public CertificatePrincipal(X509Certificate cert)
{
String file = System.Web.HttpContext.Current.Server.MapPath("certmap.config");
// 比较该文件与缓存文件的日期
FileInfo f = new FileInfo(file);
DateTime fileDate = f.LastWriteTime;
// 如果需要,重新加载
if (fileDate > _certmapDateTime)
{
XmlSerializer ser = new XmlSerializer(typeof (CertificateMapping));
using (FileStream fs = new FileStream(file,FileMode.Open,FileAccess.Read))
{
_map = (CertificateMapping) ser.Deserialize(fs);
}
_certmapDateTime = fileDate;
}
_certmap = _map[cert.GetCertHashString()];
if (_certmap == null)
{
throw new ApplicationException("The certificate " +
cert.GetCertHashString() + " has not been configured.");
}
_ident = new CertificateIdentity(cert);
}
Principal 实现中还提供一个用来访问 XML 文件中配置的角色成员身份的方法和一个用来检索 Identity 对象的属性:
public bool IsInRole
(string role)
{
return _certmap.Roles.Contains(role);
}
public System.Security.Principal.IIdentity Identity
{
get
{
return _ident;
}
}
}
CertificateIdentity 类包含 IIdentity 的一个样板化实现和一个附加属性,该属性用来访问已经用于对邮件进行签名的 X509Certificate 对象。
public class CertificateIdentity: IIdentity { private X509Certificate _x509cert; internal CertificateIdentity(X509Certificate cert) { _x509cert = cert; } public bool IsAuthenticated { get { return true; } } public string Name { get { return _x509cert.GetName(); } } public string AuthenticationType { get { return "X.509"; } } public X509Certificate Certificate { get { return _x509cert; } } }
完成所需功能的最后一步是使用服务器应用程序注册新创建的令牌管理器。安装 Web Services Enhancements 2.0 后,可以用两种不同的方法来执行此操作。可以直接编辑 web.config 文件,或者使用图 1 中介绍的 WSE 2.0 设置工具。
要使用 Visual Studio .NET 中的 WSE 2.0 设置工具,右击某个 Web 服务项目,并选择 WSE Settings 2.0(WSE 设置 2.0)。将看到如图 3 所示的对话框。确保两个复选框均被选中。
图 3:启用 Web 服务项目的 WSE 和扩展的处理流程
然后切换到 Security(安全)选项卡以指定令牌管理器的名称,如图 4 所示。
图 4:添加新的令牌管理器且检验证书设置。(单击查看大图像。)
要添加新的令牌管理器,单击 Add...(添加)并填充数据,如图 5 所示。请注意,Type(类型)项必须用以下格式指定:
<namespace>.<classname>, <assemblyname>
这使得示例的完整类型名为“RoleBasedSecurityExtension.X509RoleBasedSecurityTokenManager, RoleBasedSecurityExtension”。
图 5:添加一个新的令牌管理器
最后一步是在 Policy(策略)选项卡上配置策略文件,如图 6 所示。我们马上就会看到这个策略文件的内容。
图 6:设置策略文件名称(单击查看大图像。)
将安全令牌管理器、策略文件、自定义的 Principal 和 Identity 的身份标识对象组合在一起,允许您利用 WS-Policy 以纯描述的形式使用基于角色的安全。
本文的开始部分向您展示了传统 ASP.NET 应用程序中通过两种不同的方法使用基于角色的安全:一个是基于显式代码来检查角色,另一个是基于使用带有 .NET 属性的声明安全来指定一个方法的安全性需要。
在启用 WSE 2.0 的 Web 服务应用程序中,存在两种类似的方法。主要区别是我们不使用 .NET 属性来指定方法的安全性需要,而是借助于 WS-Policy 功能。采用这种做法的原因是该策略具有一个无与伦比的优势:它是一个可以交给要实现客户端程序的开发人员的 XML 文件,这样,开发人员就能够提前为每个 Web 服务确定需要满足的需求。
但是,让我们从基于角色成员身份检查的代码开始。不幸的是,使用类似于 SomeContext.Current.User.IsInRole() 的结构的简单方法不再可用,因为在传入的 SOAP 邮件中,可能存在多个安全令牌。而本文选择的是提供两种辅助方法。第一种方法检查当前请求的安全性上下文,并查找用于对传入邮件的全部正文进行签名的 X509SecurityToken。第二个辅助方法是一个小包装程序,提供就像使用 SomeContext.Current.User.IsInRole() 一样简单的编程接口。
public X509SecurityTokenGetBodySigningToken
(Security sec) { X509SecurityToken token = null; foreach (ISecurityElement securityElement in sec.Elements) { if (securityElement is Signature) { Signature sig = (Signature)securityElement; if ((sig.SignatureOptions & SignatureOptions.IncludeSoapBody) != 0) { SecurityToken sigToken = sig.SecurityToken; if (sigToken is X509SecurityToken) { token = (X509SecurityToken)sigToken; } } } } return token; } private boolCurrentCertificatePrincipalIsInRole
(String role) { X509SecurityToken tok = GetBodySigningToken(RequestSoapContext.Current.Security); if (tok == null) return false; if (tok.Principal == null) return false; return tok.Principal.IsInRole(role); }
现在,只要用一行代码检查发件人的 X.509 证书的角色成员身份,就可以在 [WebMethod] 中使用这些辅助方法:
[WebMethod] public void SetMonthlySalary(long employeeID, double salary) { if (! CurrentCertificatePrincipalIsInRole ("HR")
) throw new Exception("Only members of 'HR' may call this method."); if (salary > 2000) { if (! CurrentCertificatePrincipalIsInRole("PointyHairedBoss")
) throw new Exception("Only the pointy haired boss might set " + "salaries larger than 2K"); } // ... 实际工作被删除... }
最后还缺少的是,使用策略文件来以声明的方式指定 Web 服务的安全性需要。在今天的 Web 服务领域内,这是 WSE 2.0 可以为所有可用工具明确提供最佳支持的地方!不用依照 WS-Policy 手动编写策略文件,可以再次使用 WSE Settings 2.0 对话框。在该对话框中,再次切换到 Policy(策略)选项卡,并单击 Create/Edit(创建/编辑),然后单击 Add Policy(添加策略)打开如图 7 所示的对话框。
图 7:添加策略变得更容易了!
正如您在该对话框中看到的那样,您唯一要做的工作是使用基于 WS-Policy 的声明式安全来指定服务位置,选中 Require Signature(需要签名)复选框,选择签名类型,并输入访问该服务时所需的角色。
这些设置将使 Framework 根据策略文件中指定的安全性要求来检查所有传入邮件。例如,如果传入邮件没有被签名或者没有将证书分配给“Accounting”角色,将向客户端返回一个 SOAP 错误,告知传入邮件不符合策略要求。请注意:如果收到一个异常,告知证书的信任链不能被验证,则您的自定义证书颁发机构的根证书还没有被安装到“本地计算机”存储中。要解决这一问题,请转到 http://<your_certificate_server>/certsrv,并通过手动选择“导入证书”向导中的导入位置和物理导入位置来导入根证书。
本文介绍了如何创建和使用 Web Services Enhancements 2.0 for Microsoft .NET 的自定义安全令牌管理器来检查 X.509 证书,以及将它们映射到角色并用自定义的 Principal 和 Identity 对象来填充上下文信息。此外,还说明了使用 Visual Studio .NET 的 WS-Policy 将角色成员身份的声明式检查添加到您的应用程序中是多么的容易。与传统的基于 HTTP 的安全性相比较时,这种基于 WS-Security 的方法的优势在于它不依赖于传输层的完整性和安全性,而只是由 SOAP 邮件来完成。这为存在多跳点和多协议的情况提供了端对端安全功能。
Ingo Rammer 是一位具有独到见解的顾问、导师和开发人员,帮助多家软件开发和电信企业采用 Microsoft .NET 和 Web 服务技术。他协助很多欧洲和北美洲的 Fortune 500 公司和独立的软件开发商 (ISV) 过渡到 .NET。除了顾问服务以外,他还是 DevelopMentor 的讲师,并是一个遍及全世界的开发人员协会的特聘发言人,此外,他撰写了两本最畅销的书《Advanced .NET Remoting》和《Advanced .NET Remoting in VB.NET》(Apress)。Ingo 是奥地利区的 Microsoft Regional Director(英文),有关他的详细信息,请访问 http://www.ingorammer.com/(英文)。