[WCF安全系列]消息的保护等级[下篇]
在《上篇》中,我们着重讨论了消息的保护等级如果在契约中定义,定义在不同契约(服务契约、错误契约和消息契约)中的消息保护等级具有怎样的层级关系,以及在默认情况下各种绑定采用怎样的保护等级。在下篇中,我们进一步来探讨消息保护等级和绑定的关系。
一、契约的保护等级为绑定进行消息保护设置了“最低标准”
二、显式地将保护等级设置成ProtectionLevel.None与没有设置保护等级有区别吗?
三、消息的保护等级与WS-Addressing
一、契约的保护等级为绑定进行消息保护设置了“最低标准”
定义在契约上消息保护级别实际上为WCF实施消息保护设置了一个“最低标准”。由于整个消息保护机制,不论是签名还是加密,都是在信道层实现的。而信道层最终是通过绑定来实现的,绑定的属性决定了信道层处理消息的能力。而绑定安全方面的属性自然就决定了最终的信道层是否有能力对消息实施签名和加密。一方面,以契约形式定义的消息保护级别帮助信道层决定应该对传入的消息采取那个级别的保护机制;另一方面,如果绑定所能提供的消息保护能力不能达到这个最低标准,就会抛出异常。
举个例子,如果我们通过如下的代码将服务契约ICalculator的Add操作的保护级别设置成EncryptAndSign。
1: [ServiceContract(Namespace = "http://www.artech.com/")]
2: public interface ICalculator
3: {
4: [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)]
5: double Add(double x, double y);
6: }
但是我们确将终结点使用到的WS2007HttpBinding的安全模式设置成None。那么在对服务进行寄宿的时候,就会跑出如下图所示的InvalidOperationException异常,提示“必须保护请求消息”。
1: <system.serviceModel>
2: <bindings>
3: <ws2007HttpBinding>
4: <binding name="bindingWithNoneSecurityMode">
5: <security mode="None"/>
6: </binding>
7: </ws2007HttpBinding>
8: </bindings>
9: <services>
10: <service name="Artech.WcfServices.Services.CalculatorService" >
11: <endpoint address="http://127.0.0.1/calculatorservice" binding="ws2007HttpBinding" bindingConfiguration="bindingWithNoneSecurityMode"
12: contract="Artech.WcfServices.Contracts.ICalculator"/>
13: </service>
14: </services>
15: </system.serviceModel>
二、显式地将保护等级设置成ProtectionLevel.None与没有设置保护等级有区别吗?
在这里有一个很多人会忽视的要点。表示消息保护级别的ProtectionLevel类型是一个枚举,所以它肯定有一个默认值。这个默认值就是None,也就是说当你没有显式地指定契约具有采用那么保护级别的时候,默认值就是None。但是这种情况和你显式保护级别设置为None的效果是完全不一致的。因为前者真正采用的保护级别(当绑定安全被开启)实际上是EncryptAndSign,后者才是None。那么WCF如何来区分这两种情况呢?
如果你足够细心,你应该会发现:在上面介绍的定义消息保护级别的特性中,除了具有一个可读可写的ProtectionLevel属性之外,还具有一个只读的HasProtectionLevel属性,该属性表示你是否对消息保护级别进行了“显式”的设置。我们可以通过一个简单的实验来演示HasProtectionLevel的作用。
下面我定义了两个服务契约IServiceContract1和IServiceContract2,其实前者没有对ProtectionLevel进行相应的设置,后者被显式地设置为None。
1: [ServiceContract]
2: public interface IServiceContract1
3: {
4: [OperationContract]
5: void DoSomething();
6: }
7: [ServiceContract(ProtectionLevel = ProtectionLevel.None)]
8: public interface IServiceContract2
9: {
10: [OperationContract]
11: void DoSomething();
12: }
然后我编写了如下的代码,基于上面两个接口类型生成相应的ContractDescription对象,然后将它们的ProtectionLevel和HasProtectionLevel属性输出来。从最终的输出结果我们可以很清楚地看到:两种情况下下ProtectionLevel属性值都是None,但是只有当你显式地设置了ProtectionLevel的情况下,HasProtectionLevel属性才会返回True。WCF就是根据ContractDescription的这两个属性决定最终采用怎样的消息保护级别的。
1: ContractDescription contract1 = ContractDescription.GetContract(typeof(IServiceContract1));
2: ContractDescription contract2 = ContractDescription.GetContract(typeof(IServiceContract2));
3:
4: Console.WriteLine("{0,-10}{1,-20}{2,-20}", "Contract","ProtectionLevel", "HasProtectionLevel");
5:
6: Console.WriteLine("{0,-10}{1,-20}{2,-20}", "contract1", contract1.ProtectionLevel, contract1.HasProtectionLevel);
7: Console.WriteLine("{0,-10}{1,-20}{2,-20}", "contract2", contract2.ProtectionLevel, contract2.HasProtectionLevel);
输出结果:
1: Contract ProtectionLevel HasProtectionLevel
2: contract1 None False
3: contract2 None True
三、消息的保护等级与WS-Addressing
关于消息保护级别与绑定的关系,还有一点需要着重强调。虽然我们可以对于同一个服务契约下操作设置不同的保护级别,但是在WSDL中需要基于WS-Addressing中的寻址(Addressing)机制来识别基于操作的保护级别。在使用的绑定不支持WS-Addressing的情况下(比如BasicHttpBinding),它会选择所有操作中等级最高的那个作为所有操作的保护级别。比如说对于如下定义的服务契约ICalculator,在使用BasicHttpBinding的情况下,两个操作采用的保护级别都是EncryptAndSign。
1: [ServiceContract]
2: public interface ICalculator
3: {
4: [OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
5: double Add(double x, double y);
6: [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)]
7: double Substract(double x, double y);
8: }
这实际上会为你的应用带来一个很隐晦的问题,为了将这个问题阐述得更加清楚,我通过一个例子来说明。还是应用我们的计算服务的例子,下面是我们再熟悉不过的服务契约的定义,Add操作的保护级别被设置成Sign。
1: [ServiceContract(Namespace = "http://www.artech.com/")]
2: public interface ICalculator
3: {
4: [OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
5: double Add(double x, double y);
6: }
但是这个服务契约并被客户端共享,而客户端服务契约中定义了一个额外的操作Substract,该操作的保护级别并未作显式设置。
1: [ServiceContract(Namespace = "http://www.artech.com/")]
2: public interface ICalculator
3: {
4: [OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
5: double Add(double x, double y);
6: [OperationContract]
7: double Substract(double x, double y);
8: }
现在选择BasicHttpBinding作为终结点的绑定,并将安全模式甚至成Message。当你客户端调用Add操作的时候。会抛出如下图所示的MessageSecurityException异常,提示“主签名必须加密”。但是当你将客户端Substract删除或者将Substract操作的消息保护级别也设置成Sign是,这个异常将不会出现。
出现这样的异常的原因在于:对于不支持WS-Addressing的BasicHttpBinding来说,会选择所有操作中等级最高的那个最为所有操作的保护级别。对于客户端来说,由于Substract没有对保护级别进行显式设置,默认采用最高等级的EncryptAndSign。但是服务端的等级确是Sign。
在这种情况下,请求消息会同时被加密和签名。请求消息被服务端接受之后,虽然它对应的等级是Sign,但是依然能够处理该请求。这就是所谓的“消息保护级别的最低标准”原则,定义在契约中的保护级别只是确立了一个消息保护的“底线”。你不能低于这个最低标准,但是可以高于它。但是服务执行正常的运算后,只会按照定义在本地契约中设置的保护级别对回复消息进行签名。客户端接受到这个仅仅被签名的回复消息,会发现等级不够,所以才会提示你“主签名必须加密”。