WCF 服务行为示例:IPFilter - 按 IP 地址允许/拒绝访问






4.92/5 (17投票s)
WCF 服务行为示例:IPFilter - 按 IP 地址允许/拒绝访问
引言
今天,我的一个同事就他正在进行的一项任务寻求帮助。他需要能够识别客户端是否在 IP 块中。他正在使用来自 blockacountry.com 的数据,并希望服务根据客户端 IP 执行不同的操作。
在我们的示例解决方案中,我们将创建一个可重用的 WCF 服务行为,可用于根据客户端 IP 地址拒绝或允许使用服务。服务行为允许我们通过配置以声明方式将我们的 IPFilter
添加到任何服务。我们还将创建配置节以支持按 IP 允许/拒绝,类似于 ASP.NET 中的授权配置元素。
不应在应用程序级别阻止 IP 地址。如果可能,您应该使用防火墙。如果您的服务/应用程序托管在 IIS 中,您可以使用它提供的内置 IP 过滤。您可以在此处了解有关 IIS IP 限制的更多信息。如果您在 IIS 之外托管 WCF 服务或开发基于套接字的应用程序,这可能也对您有用。
背景
我们首先需要了解一些 IP 地址基础知识。IP 地址是用于唯一标识网络中计算机的数字。IP 版本 4 地址是 32 位宽,但很快将被 IP 版本 6 取代,后者使用 128 位数字作为地址。今天我们只关注 IPV4。尽管 IP 地址是一个 32 位数字,但将其写成十进制并用句点分隔数字的每个字节是最常见的书写方式。这称为点分十进制表示法。
点分十进制 | 十六进制 |
127.0.0.1 | 0x100007F |
24.1.5.1 | 0x1050118 |
192.168.0.23 | 0x1700A8C0 |
IP 地址不仅是特定主机/计算机的标识符,也是主机所在网络的标识符。IPV4 允许我们将流量分段为称为子网的较小组。地址的开头是组/子网标识符,地址的结尾是主机标识符。每个 IPV4 地址都有一个对应的子网掩码或网络掩码。网络掩码是用于标识 IP 地址的子网标识符部分的位掩码。
网络掩码可用于确定子网中可以有多少主机。子网 255.255.255.0 或 0xFFFFFF00 留下 1 个字节或 256 个可能的地址(包括零)。其中一些是保留的广播地址,不能使用。
一种在不包含网络掩码的情况下描述子网的更有效方法是 CIDR(无类别域间路由)表示法。由于子网标识符始终位于 IP 地址的开头并且必须是连续的,因此我们只需要知道位数。使用 CIDR 表示法,我们以点分十进制表示法写出 IP 地址的子网标识符部分,然后是正斜杠和子网标识符使用的位数。
CIDR 不仅仅是一种表示法。在旧时代(1993 年之前),IP 地址网络和主机标识符只能沿着称为类的 8 位边界进行分段。CIDR 中的“无类别”源于您不受 8 位类限制,可以随意拆分位的事实。您可以在此处了解更多信息。
IP 验证代码
我们的新代码将围绕一个名为 IPRange
的 struct
。这个 struct
将描述 IP 范围。这个 struct
的代码如下所示
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 8)]
public struct IPRange
{
private static readonly char[] _wildcardChars = new char[] { '*', 'x', 'X' };
private readonly uint _addressMask;
private readonly byte _maskData;
private readonly Mode _mode;
private IPRange(uint mask, byte maskData, Mode mode)
{
_addressMask = mask;
_maskData = maskData;
_mode = mode;
}
...
我们需要支持地址中间的通配符。例如 192.168.*.11 或 10.*.11.*。我的同事说这是一个要求,所以我们必须实现它。由于这个后来的添加,我引入了一个模式标志来在“类”模式和无类模式之间切换。
测试匹配
我们根据模式以不同的方式测试匹配。在无类别模式下,我们简单地将 IP 地址的主机标识符部分向右移,这样我们就只剩下网络标识符并进行比较。由于类模式通配符不与网络和主机标识符绑定(例如 192.168.*.1),我们需要以不同的方式比较它们。在类模式下,我们使用位掩码,但我们还必须检查地址中的零。另一种可能的方法是枚举每个位,检查它是否在网络/通配符掩码中,如果存在则比较位。这将为我们提供一个统一的解决方案。
public bool IsMatch(uint address)
{
if (_mode == Mode.Class)
{
// Check the mask.
if ((address & _addressMask) != _addressMask)
{
return false;
}
// Check for zeros in mask
IPClass ipClasses = (IPClass)_maskData;
if ((ipClasses & IPClass.A) != IPClass.A &&
(0xFF & _addressMask) == 0 && (0xFF & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.B) != IPClass.B &&
(0xFF00 & _addressMask) == 0 && (0xFF00 & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.C) != IPClass.C &&
(0xFF0000 & _addressMask) == 0 && (0xFF0000 & address) != 0)
{
return false;
}
if ((ipClasses & IPClass.D) != IPClass.D &&
(0xFF000000 & _addressMask) == 0 && (0xFF000000 & address) != 0)
{
return false;
}
return true;
}
// Shift over the host identifier and so we are only
// comparing the network identifier portion of the ip address
int shift = 32 - _maskData;
return (_addressMask << shift) == (address << shift);
}
解析通配符和 CIDR 地址
下面是我们解析通配符和 CIDR 地址的代码
public static IPRange Parse(string value)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("value");
}
// Check if is a wildcard.
if (IsWildCard(value))
{
return new IPRange(0, 0xF, Mode.Class);
}
int index = value.IndexOf('/');
Mode mode;
byte classData;
uint mask;
if (index > -1)
{
ParseAddress(value.Substring(0, index), out classData, out mask);
mode = Mode.Classless;
if (classData != 0)
{
throw new ArgumentException(
string.Format("You can use a CIDR notation and wildcards.
'{0}' is invalid'.", value),
"value");
}
if (!byte.TryParse(value.Substring(index + 1), out classData))
{
throw new ArgumentException(
string.Format("The '{0}' is invalid'.", value),
"value");
}
if (classData > 31)
{
throw new ArgumentException
("The subnet mask length must be less than 32.", "value");
}
// Remove any bits that are not apart of the network identifier
mask &= (uint)((1 << classData) - 1);
}
else
{
ParseAddress(value, out classData, out mask);
mode = Mode.Class;
}
return new IPRange(mask, classData, mode);
}
private static void ParseAddress(string value,
out byte ipClassWildcards, out uint address)
{
string[] values = value.Split('.');
if (values.Length != 4)
{
throw new ArgumentException(string.Format
("The ip address is in invalid format {0}", value), "value");
}
byte ipValue;
byte classIndex = 1;
int maskIndex = 0;
address = 0;
ipClassWildcards = 0;
for (int i = 0; i < values.Length; i++)
{
if (IsWildCard(values[i]))
{
ipClassWildcards |= classIndex;
}
else
{
if (!byte.TryParse(values[i], out ipValue))
{
throw new ArgumentException(string.Format
("The ip address is in invalid format {0}", value), "value");
}
address |= (uint)(ipValue << maskIndex);
}
maskIndex += 8;
classIndex <<= 1;
}
}
可用地址
下面是我们用于获取子网中可用地址数量的代码。我们计算可用的主机位,创建所有这些位的数字,并为零加一。
public int Count
{
get
{
if (_mode == Mode.Classless)
{
return (1 << (32 - _maskData));
}
int count = 0;
for (int i = 0; i < 4; i++)
{
if (((1 << i) & _maskData) != 0)
{
count += 1;
}
}
return (1 << (count * 8));
}
}
为了获取 IPRange
中所有可能的 IPAddresses
,我们需要识别通配符位并枚举所有可能的值。在无类别模式下,我们获取地址计数并循环遍历这些地址,将索引移位并与掩码进行按位或运算。类模式的代码更多一些,因为通配符可以位于任何位置。在类模式下,我们枚举每个类的可能值并进行按位或运算以生成最终结果。
public IEnumerable<uint> GetAddressValues()
{
if (_mode == Mode.Class)
{
IPClass classWildcard = (IPClass)_maskData;
int aStart, aEnd, bStart, bEnd, cStart, cEnd, dStart, dEnd;
GetClassRange(IPClass.A, out aStart, out aEnd);
GetClassRange(IPClass.B, out bStart, out bEnd);
GetClassRange(IPClass.C, out cStart, out cEnd);
GetClassRange(IPClass.D, out dStart, out dEnd);
for (int a = aStart; a <= aEnd; a++)
{
for (int b = bStart; b <= bEnd; b++)
{
for (int c = cStart; c <= cEnd; c++)
{
for (int d = dStart; d <= dEnd; d++)
{
yield return (uint)(a | b << 8 | c << 16 | d << 24);
}
}
}
}
}
else
{
int maxValue = Count;
for (int i = 0; i < maxValue; i++)
{
yield return (uint)(((uint)i << _maskData) | _addressMask);
}
}
}
private void GetClassRange(IPClass ipClass, out int start, out int end)
{
if ((ipClass & (IPClass)_maskData) == ipClass)
{
start = 0;
end = 255;
}
else
{
int shift = ((byte)ipClass - 1) * 8;
start = end = (_maskData >> shift) & 0xFF;
}
}
IPFilter
IPFilter
类接受多个 IPRange
并将其与允许/拒绝行为关联。这允许我们针对多个 IPRange
验证一个 IP 地址。这些按自上而下的顺序进行评估。当找不到匹配项时,返回默认行为。
public class IPFilter
{
private string _name;
private IList<IPFilterItem> _items;
private IPFilterType _defaultBehavior;
...
public enum IPFilterType
{
NoMatch = 0,
Deny = 1,
Allow = 2
}
public class IPFilterItem
{
private IList<IPRange> _ranges;
private IPFilterType _result;
...
IPFilter
也是可配置的。下面是一些示例配置
<?xml version="1.0" encoding="utf-8" ?>
<!-- Example configuration -->
<configuration>
<configSections>
<section name="IPFilter"
type="IPFilter.Configuration.IPFilterConfiguration,IPFilter"/>
</configSections>
<IPFilter>
<HttpModule FilterName="Default" />
<Filters>
<add Name="Default" DefaultBehavior="Deny">
<deny hosts="192.168.11.12,192.168.1.*" />
<allow hosts="192.168.0.0/16" />
<deny hosts="*" />
</add>
<!-- A filter than only allows traffic from local network -->
<add Name="LocalOnly">
<allow hosts="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/8" />
<deny hosts="*" />
</add>
<!-- A filter than denies traffic from local network -->
<add Name="DenyLocal">
<deny hosts="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/8" />
<allow hosts="*" />
</add>
<!-- A filter than only allows traffic from loopback -->
<add Name="LoopbackOnly">
<allow hosts="127.0.0.1/8" />
<deny hosts="*" />
</add>
<!-- A filter than denies traffic from loopback -->
<add Name="DenyLoopback">
<deny hosts="127.0.0.1/8" />
<allow hosts="*" />
</add>
</Filters>
</IPFilter>
</configuration>
WCF 服务行为
服务行为允许我们连接到 WCF 服务的各个部分并修改其行为。下面是 IServiceBehavior
接口
public interface IServiceBehavior
{
void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters);
void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase);
void Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase);
}
我们将创建一个服务行为,它将插入一个 IDispatchMessageInspector
。为此,我们需要实现 ApplyDispatchBehavior
方法。
public void ApplyDispatchBehavior
(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher channelDispatcher
in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher endpointDispatcher
in channelDispatcher.Endpoints)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
}
}
IDispatchMessageInspector
只有两个方法,AfterReceiveRequest
和 BeforeSendReply
。AfterReceiveRequest
在消息首次进入时触发。消息通过 ref 传入,允许替换或将其设置为 null
。当消息设置为 null
时,服务方法不会被调用。我们将检查 IP 地址,如果它在我们的拒绝列表中,我们将把消息设置为 null
。此方法返回一个对象,该对象在调用服务方法后传递给 BeforeSendReply
,允许我们在两个方法之间持久化一些状态。
public object AfterReceiveRequest(ref Message request,
IClientChannel channel, InstanceContext instanceContext)
{
// RemoteEndpointMessageProperty new in 3.5 allows us
// to get the remote endpoint address.
RemoteEndpointMessageProperty remoteEndpoint = request.Properties
[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
// The address is a string so we have to parse to get as a number
IPAddress address = IPAddress.Parse(remoteEndpoint.Address);
// If IP address is denied clear the request message
// so service method does not get execute
if (_verifier.CheckAddress(address) == IPFilterType.Deny)
{
request = null;
return (channel.LocalAddress.Uri.Scheme.Equals(Uri.UriSchemeHttp) ||
channel.LocalAddress.Uri.Scheme.Equals(Uri.UriSchemeHttps)) ?
_httpAccessDeined : _accessDenied;
}
return null;
}
我们实际上在 BeforeSendReply
方法中没有做任何事情。如果通道是 http
,则我们将状态码设置为 401
。
public void BeforeSendReply(ref Message reply, object correlationState)
{
if (correlationState == _httpAccessDeined)
{
HttpResponseMessageProperty responseProperty =
new HttpResponseMessageProperty();
responseProperty.StatusCode = (HttpStatusCode)401;
reply.Properties["httpResponse"] = responseProperty;
}
}
服务行为可以通过代码应用,但理想的方式是应用程序配置。WCF 允许我们为服务行为创建强类型配置节。这些配置节应继承自 BehaviorExtensionElement
。它继承自 ServiceModelExtensionElement
,而后者又继承自 ConfigurationElement
。BehaviorExtensionElement
有一个我们需要实现的 abstract
方法和属性。该属性返回实现 IServiceBehavior
的类的类型。abstract
方法应返回此对象的新实例。BehaviorExtensionElement
节可以嵌套在 system.serviceModel
配置节中的 extensions 元素下。
下面是一些示例配置和我们的 BehaviorExtensionElement
的完整内容。
...
<extensions>
<behaviorExtensions>
<add
FilterName="LocalOnly"
type="IPFiltering.IPFilterBehaviorExtension, IPFilter,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
/>
</behaviorExtensions>
</extensions>
...
public class IPFilterBehaviorExtension : BehaviorExtensionElement
{
[ConfigurationProperty("filterName", IsRequired = true)]
public string FilterName
{
get
{
return this["filterName"] as string;
}
set
{
this["providerName"] = value;
}
}
public override Type BehaviorType
{
get
{
return typeof(IPFilterServiceBehavior);
}
}
protected override object CreateBehavior()
{
return new IPFilterServiceBehavior(this.FilterName);
}
}
ASP.NET 模块
示例代码还包括一个使用 IPFilter
类的 ASP.NET 模块。同样,IIS 内置了IP 过滤,如果可能,您应该使用它。
链接
- 子网:http://en.wikipedia.org/wiki/Subnetwork
- 点分十进制表示法:http://en.wikipedia.org/wiki/Dot-decimal_notation
- CIDR 表示法:http://en.wikipedia.org/wiki/CIDR_notation
- 无类别域间路由:http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
- 按国家/地区阻止 IP 地址:http://www.blockacountry.com/
- IIS IP 安全:http://www.iis.net/ConfigReference/system.webServer/security/ipSecurity
IServiceBehavior
接口:http://msdn.microsoft.com/en-us/library/system.servicemodel.description.iservicebehavior.aspxIDispatchMessageInspector
接口:http://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.idispatchmessageinspector.aspxBehaviorExtensionElement
类:http://msdn.microsoft.com/en-us/library/system.servicemodel.configuration.behaviorextensionelement.aspx
历史
- 2009 年 6 月 15 日: 初始发布