为带用户名令牌的 SOAP 消息签名和加密自定义 WSE 3.0 策略断言






4.89/5 (36投票s)
2005 年 11 月 8 日
8分钟阅读

488735

2462
一篇描述如何实现自定义 WSE 3.0 策略断言,用于为带用户名令牌的 SOAP 消息进行签名和加密的文章。
引言
本文旨在分享我对 WSE 3.0 的研究成果,并提供一个真实的、可工作的 Web 服务安全实现示例。
WSE 3.0 库提供了基于 X.509 证书、SSL 上的用户名令牌或 Kerberos 票证的多种“即插即用”安全机制。所有这些(如 X.509、SSL、Kerberos 票证)在实验室环境下或者在完全受控的部署目标环境中是很好的。相对简单的应用程序需要更简单的实现(但安全性不减 ;-)),因此我们采用基于用户名和密码衍生的用户名令牌来实现安全。这种技术不需要对托管环境有任何特定要求。
本文将回答以下问题:
- 如何实现自定义 WSE 3.0 策略断言类
- 如何从策略文件中读取断言配置设置
- 如何实现用于自定义策略断言的输出客户端和输入服务 SOAP 过滤器
- 如何使用用户名令牌对 SOAP 消息进行签名
- 如何使用用户名令牌加密消息正文
- 如何使用用户名令牌加密自定义 SOAP 标头
- 如何实现用于验证和解密客户端 SOAP 请求的用户名令牌管理器
- 如何配置客户端和服务器应用程序以启用安全
背景
当然,本文要求您对 SOAP Web 服务有良好的理解,并且具备 WSE 2.0 库的一些经验。但是,本文的代码是独立的,可以在您的应用程序中稍作修改或不作修改地使用。
在将此代码包含到您的生产应用程序之前,请参阅WSE 开发中心,获取有关此技术及其用法的更多信息。
运行以下示例所需的 WSE 3.0 October CTP 库(最新的 WSE 版本)可以从此处下载。请注意,该库仍处于 Beta 阶段,但与 .NET 2.0 RTM 配合使用效果很好。微软承诺 WSE 将在 VS.NET 2005 正式发布日期(目前是 2005 年 11 月 7 日)前后发布。
因此,不要浪费时间,立即开始学习这项激动人心的新技术,并迁移您几乎已成为遗留的 ;-) WSE 2.0 代码!
使用代码
在描述代码片段之前,我想简单介绍一下 WSE 3.0 整体架构的基本思想。初次接触 WSE 3.0 可能会觉得很难、很耗时才能让它工作,并且没有动力去实现。但经过一些时间的研究和代码实验后,您会意识到 WSE 3.0 相较于之前的 WSE 2.0 是一个巨大的进步,能够编写更少的代码,并提供更灵活的配置。
WSE 3.0 基于策略。每个策略都可以应用于 Web 服务类或 Web 服务代理(客户端)类。策略是一组断言。我认为“断言”一词在此处不太合适,但事实证明断言只是一个工厂类,用于创建将注入 SOAP 处理管道的输入/输出 SOAP 过滤器。所有策略都可以声明在一个单独的“策略缓存”文件中,然后通过特性(attribute)分配给 Web 服务类,或者以编程方式分配给代理类。策略缓存文件是一个 XML 文档,包含:
- 一组所谓的“扩展”,它们仅仅是命名断言及其相应类型名称的列表;以及
- 策略本身,每个策略都是扩展(断言)的列表,其中包含相应的可选配置元素。
好了,让我们开始编写代码吧!第一步是实现一个应用于 Web 服务代理的自定义断言。断言的主要功能是构造四个 SOAP 过滤器:
- 客户端输出过滤器(处理客户端发出的 SOAP 请求)
- 客户端输入过滤器(处理服务接收到的 SOAP 响应)
- 服务输入过滤器(处理客户端收到的 SOAP 请求)
- 服务输出过滤器(处理 Web 服务发出的 SOAP 响应)
断言的第二个可选功能是从策略缓存文件中解析断言配置元素。如果断言是通过策略缓存文件声明式应用的话,则需要这一步。
我们的客户端用户名断言的代码
public class UsernameClientAssertion : SecurityPolicyAssertion
{
private string username;
private string password;
public UsernameClientAssertion(string username, string password)
{
this.username = username;
this.password = password;
}
public override SoapFilter
CreateClientOutputFilter(FilterCreationContext context)
{
return new ClientOutputFilter(this, context);
}
public override SoapFilter
CreateClientInputFilter(FilterCreationContext context)
{
// we don't provide ClientInputFilter
return null;
}
public override SoapFilter
CreateServiceInputFilter(FilterCreationContext context)
{
// we don't provide any processing for web service side
return null;
}
public override SoapFilter
CreateServiceOutputFilter(FilterCreationContext context)
{
// we don't provide any processing for web service side
return null;
}
...
}
由于我们的客户端断言需要用户名和密码,它将被命令式地在代码中分配给代理,因此该断言有一个构造函数用于传递凭据。凭据仅存储在私有字段中,然后可以在输出过滤器中访问。
我们的客户端断言仅提供一个“ClientOutputFilter
”。客户端输出过滤器的主要目的是:
- 创建用户名令牌。
- 使用用户名令牌对 SOAP 消息进行签名。
- 加密 SOAP 正文。
- 加密标记有特殊命名空间的自定义 SOAP 标头。
客户端输出过滤器的代码
class ClientOutputFilter : SendSecurityFilter
{
UsernameClientAssertion parentAssertion;
FilterCreationContext filterContext;
public ClientOutputFilter(UsernameClientAssertion parentAssertion,
FilterCreationContext filterContext)
: base(parentAssertion.ServiceActor, false, parentAssertion.ClientActor)
{
this.parentAssertion = parentAssertion;
this.filterContext = filterContext;
}
public override void SecureMessage(SoapEnvelope envelope, Security security)
{
UsernameToken userToken = new UsernameToken(
parentAssertion.username,
parentAssertion.password,
PasswordOption.SendNone);
// we don't send password over network
// but we just use username/password to sign/encrypt message
// Add the token to the SOAP header.
security.Tokens.Add(userToken);
// Sign the SOAP message by using the UsernameToken.
MessageSignature sig = new MessageSignature(userToken);
security.Elements.Add(sig);
// encrypt BODY
EncryptedData data = new EncryptedData(userToken);
// encrypt custom headers
for (int index = 0; index <
envelope.Header.ChildNodes.Count; index++)
{
XmlElement child =
envelope.Header.ChildNodes[index] as XmlElement;
// find all SecureSoapHeader headers
// marked with a special attribute
if (child != null && child.NamespaceURI ==
"http://company.com/samples/wse/")
{
// create ID attribute for referencing purposes
string id = Guid.NewGuid().ToString();
child.SetAttribute("Id", "http://docs.oasis-" +
"open.org/wss/2004/01/oasis-200401-" +
"wss-wssecurity-utility-1.0.xsd", id);
// Create an encryption reference for the custom SOAP header.
data.AddReference(new EncryptionReference("#" + id));
}
}
// add ancrypted data to the security context
security.Elements.Add(data);
}
}
将断言过滤器定义为内部类是非常合适的,因为它们的名称可以更简洁,并且它们可以访问父断言的字段。
所有标头都应派生的基本 SOAP 标头类,用于安全通信,看起来如下:
/// <summary>
/// This is base class for all custom SOAP headers
/// that should be encrypted in the response.
/// </summary>
public class SecureSoapHeader : SoapHeader
{
/// <summary>
/// This property is just a flag telling us
/// that this SOAP header should be encrypted.
/// </summary>
[XmlAttribute("SecureHeader",
Namespace="http://company.com/samples/wse/")]
public bool SecureHeader;
}
加密自定义 SOAP 标头的代码在这里相当独特。我曾尝试在网上搜索相关信息,但没有成功。然后我决定写自己的解决方案。我花了一段时间才意识到如何使用标准功能来实现它。代码基于 WSE 3.0 文档中提供的有关如何签名自定义 SOAP 标头的示例。在处理签名自定义标头时,我注意到提供的示例中至少有两个不一致之处:
- 在自定义 SOAP 标头中,您无法声明一个自定义“
Id
”属性,并将其映射到 XML“Id
”属性,使用此命名空间。当然,您可以这样做,但这没有意义,因为当初始 SOAP 消息序列化时,此系统命名空间会以随机前缀重新声明,而不是预期的“wsu”前缀。解决此问题的方法是:如果您想为自定义标头添加一个服务的“wsu:id
”属性,您应该在“wsu
”命名空间声明之后进行。 - 第二个不一致之处在于,当您创建
SignatureReference
或EncryptionReference
并将其添加到相应的集合时,您应该以“#[id]”(其中 [id] 是节点的唯一标识符 - GUID)的形式指定引用 URI,而不是像文档建议的那样使用“#Id:[id]”。
以上是客户端所需的所有代码。下面将给出如何为客户端指定此断言的代码。现在,让我们转向 Web 服务端,并考虑服务端断言。UsernameServiceAssertion
的代码是:
public class UsernameServiceAssertion : SecurityPolicyAssertion
{
public UsernameServiceAssertion()
{
}
public override SoapFilter
CreateClientOutputFilter(FilterCreationContext context)
{
// we don't provide any processing for client side
return null;
}
public override SoapFilter
CreateClientInputFilter(FilterCreationContext context)
{
// we don't provide any processing for client side
return null;
}
public override SoapFilter
CreateServiceInputFilter(FilterCreationContext context)
{
return new ServiceInputFilter(this, context);
}
public override SoapFilter
CreateServiceOutputFilter(FilterCreationContext context)
{
// we don't provide ServiceOutputFilter
return null;
}
public override void ReadXml(XmlReader reader,
IDictionary<string, Type> extensions)
{
if (reader == null)
throw new ArgumentNullException("reader");
if (extensions == null)
throw new ArgumentNullException("extensions");
// determine the name of the extension
string tagName = null;
foreach (string extName in extensions.Keys)
{
if (extensions[extName] ==
typeof(UsernameServiceAssertion))
{
tagName = extName;
break;
}
}
// read the first element (maybe empty)
reader.ReadStartElement(tagName);
}
public override void WriteXml(XmlWriter writer)
{
// Typically this is not needed for custom policies
}
...
}
注意与客户端断言的区别:我们只返回服务输入过滤器,并添加了两个额外的方法来处理断言配置元素。在我们的示例中,ReadXml
方法只是读取一个空的 XML 元素来声明策略中的断言。此外,文档中并未提及这两个方法,但当您在策略缓存文件中声明式地应用断言时,如果未重写 ReadXml
方法,则会失败。
服务输入过滤器的主要目的是检查哪些 SOAP 元素被签名和加密(为了简化示例,我已移除检查加密元素的代码)。
ServiceInputFilter
类的代码
public class ServiceInputFilter : ReceiveSecurityFilter
{
UsernameServiceAssertion parentAssertion;
FilterCreationContext filterContext;
public ServiceInputFilter(UsernameServiceAssertion parentAssertion,
FilterCreationContext filterContext)
: base(parentAssertion.ServiceActor, false,
parentAssertion.ClientActor)
{
this.parentAssertion = parentAssertion;
this.filterContext = filterContext;
}
public override void ValidateMessageSecurity(SoapEnvelope envelope,
Security security)
{
bool IsSigned = false;
if (security != null)
{
foreach (ISecurityElement element in security.Elements)
{
if (element is MessageSignature)
{
// The given context contains a Signature element.
MessageSignature sign = element as MessageSignature;
if (CheckSignature(envelope, security, sign))
{
// The SOAP message is signed.
if (sign.SigningToken is UsernameToken)
{
// The SOAP message is signed
// with a UsernameToken.
IsSigned = true;
}
}
}
}
}
if (!IsSigned)
throw new SecurityFault("Message did" +
" not meet security requirements.");
}
private bool CheckSignature(SoapEnvelope envelope,
Security security, MessageSignature signature)
{
//
// Now verify which parts of the message were actually signed.
//
SignatureOptions actualOptions = signature.SignatureOptions;
SignatureOptions expectedOptions = SignatureOptions.IncludeSoapBody;
if (security != null && security.Timestamp != null)
expectedOptions |= SignatureOptions.IncludeTimestamp;
//
// The <Action> and <To> are required addressing elements.
//
expectedOptions |= SignatureOptions.IncludeAction;
expectedOptions |= SignatureOptions.IncludeTo;
if (envelope.Context.Addressing.FaultTo != null &&
envelope.Context.Addressing.FaultTo.TargetElement != null)
expectedOptions |= SignatureOptions.IncludeFaultTo;
if (envelope.Context.Addressing.From != null &&
envelope.Context.Addressing.From.TargetElement != null)
expectedOptions |= SignatureOptions.IncludeFrom;
if (envelope.Context.Addressing.MessageID != null &&
envelope.Context.Addressing.MessageID.TargetElement != null)
expectedOptions |= SignatureOptions.IncludeMessageId;
if (envelope.Context.Addressing.RelatesTo != null &&
envelope.Context.Addressing.RelatesTo.TargetElement != null)
expectedOptions |= SignatureOptions.IncludeRelatesTo;
if (envelope.Context.Addressing.ReplyTo != null &&
envelope.Context.Addressing.ReplyTo.TargetElement != null)
expectedOptions |= SignatureOptions.IncludeReplyTo;
//
// Check if the all the expected options are the present.
//
return ((expectedOptions & actualOptions) == expectedOptions);
}
}
过滤器类也实现为 UsernameServiceAssertion
的内部类。
为了在服务侧验证用户名令牌,您应该提供用户名令牌管理器类的自定义实现。例如,它可以如下实现:
public class ServiceUsernameTokenManager : UsernameTokenManager
{
/// <summary>
/// Constructs an instance of this security token manager.
/// </summary>
public ServiceUsernameTokenManager()
{
}
/// <summary>
/// Constructs an instance of this security token manager.
/// </summary>
/// <param name="nodes">An XmlNodeList containing
/// XML elements from a configuration file.</param>
public ServiceUsernameTokenManager(XmlNodeList nodes)
: base(nodes)
{
}
/// <summary>
/// Returns the password or password equivalent for the username provided.
/// </summary>
/// <param name="token">The username token</param>
/// <returns>The password (or password equivalent) for the username</returns>
protected override string AuthenticateToken(UsernameToken token)
{
string username = token.Username;
// it's up to you where you will get a password for some user
// you may:
// 1) get the password hash from web.config or system registry
// if you are implementing per-server security
// 2) get the password from the database or XML file for the given user name
// for example purposes we just return a reversed value of username
char[] ch = username.ToCharArray();
Array.Reverse(ch);
return new String(ch);
}
}
AuthenticateToken
方法的目标是返回与用户名对应的密码。
现在,让我们看看如何使用上述所有逻辑来保护客户端和服务器之间的通信。
第一步是在 app.config 和 web.config 文件中分别为客户端和服务启用 WSE 3.0 支持。
- 添加 WSE 配置节处理程序。
- 在
<webServices>
元素中为 WSE 添加<soapServerProtocolFactory>
元素(仅限服务)。 - 添加 WSE 配置节。
- 指定自定义安全令牌管理器类(仅限服务)。
- 指定策略缓存文件的名称(仅限服务,客户端也可能需要,但本例中不涉及)。
有关如何启用 WSE 支持的更多详细信息,您可以检查随附文章示例中的 app.config 和 web.config 文件。
第二步是创建一个策略缓存文件供 Web 服务使用。对于我们的示例,它可以如下所示:
<policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
<extensions>
<extension name="usernameAssertion"
type="UsernameAssertionLibrary.UsernameServiceAssertion,
UsernameAssertionLibrary" />
</extensions>
<policy name="ServerPolicy">
<usernameAssertion />
</policy>
</policies>
第三步是使用“Policy
”特性将策略应用于 Web 服务,如下所示:
...
using Microsoft.Web.Services3;
[WebService(Namespace = "http://company.com/samples/wse/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[Policy("ServerPolicy")]
// we define policy on service level,
// so each service method will be covered by it
public class Service : System.Web.Services.WebService
// look, you don't need to inherit
// your service from some custom class
{
...
第四步是将策略应用于 Web 服务代理
// create web service proxy
// NOTE!!! When updating web reference in Visual Studio,
// don't forget to change its base class
// to Microsoft.Web.Services3.WebServicesClientProtocol then
WseSample.Service srv = new WseSample.Service();
// create custom SOAP header and assign it to web service
WseSample.BankAccountSettings settings =
new WseSample.BankAccountSettings();
settings.PinCode = "1111";
srv.BankAccountSettingsValue = settings;
// create custom policy assertion and assign it to proxy
// for password we just use reversed username
// it's important, because UsernameTokenManager
// on the service side applies the same logic
// when looking for user password
UsernameClientAssertion assert =
new UsernameClientAssertion("admin", "nimda");
// create policy
Policy policy = new Policy();
policy.Assertions.Add(assert);
// and set it to web service
srv.SetPolicy(policy);
// invoke web service method
bool valid = srv.CheckAccountStatus("123456");
就这样!现在,您 Web 服务的所有调用都已通过身份验证,并且所有敏感的请求信息都已加密!您可以通过检查服务和客户端的 InputTrace.webinfo 和 OutputTrace.webinfo 跟踪文件来验证这一点。
结论
我想提供一些关于如何改进本文示例的建议:
- 您可以在 Web 服务中添加授权逻辑。只需在
UsernameTokenManager
中添加创建自定义IPrincipal
的代码,并在 Web 方法中添加角色检查。 - 签名和加密 SOAP 响应。您可以实现额外的服务输出和客户端输入过滤器来对 SOAP 响应进行签名/加密,并在客户端验证它们。
- 压缩断言。您可以使用
System.IO.Compression
类编写自定义断言来压缩/解压缩 SOAP 请求/响应。 - 您可以实现断言,使身份验证/授权能够与用 Perl、PHP 和 VBScript(SOAP Toolkit)编写的 SOAP 客户端协同工作。
历史
- 2005/11/06 – 文章的初始版本。