zoukankan      html  css  js  c++  java
  • 构建安全的 Web Services

    更新日期: 2004年04月12日
    本页内容
    本模块内容 本模块内容
    目标 目标
    适用范围 适用范围
    如何使用本模块 如何使用本模块
    概述 概述
    威胁与对策 威胁与对策
    设计注意事项 设计注意事项
    输入验证 输入验证
    身份验证 身份验证
    授权 授权
    敏感数据 敏感数据
    参数操纵 参数操纵
    异常管理 异常管理
    审核与日志记录 审核与日志记录
    代理服务器的注意事项 代理服务器的注意事项
    代码访问安全的注意事项 代码访问安全的注意事项
    部署注意事项 部署注意事项
    小结 小结
    其他资源 其他资源

    本模块内容

    越来越多的公司和组织开始创建和部署 Web Services。通常,这些工作是在还未完全了解安全含义的情况下进行的。本模块解释了如何安全地设计、配置和部署 Web Services。对于任何接受外部请求的应用程序来说,输入验证十分重要,为此人们开发了几种技术来确保只接受格式正确的请求。本模块还详细解释了各种身份验证方法,这些方法可以用来将 Web Services 访问权仅限于授权用户,并确保记录和审核这些用户的行为。

    本模块还讨论了 Microsoft 的 Web Services Enhancements 1.0 for Microsoft® .NET (WSE),它支持 WS-Security(Web Services 安全)和一组相关的最新标准。

    目标

    使用本模块可以实现:

    设计和部署安全的 Web Services。

    使用强类型参数和 XSD 架构验证 Web Services 输入。

    对 Web Services 客户端进行身份验证。

    Web Services 访问授权。

    保护 Web Services 消息的保密性和完整性。

    按照部署方案(Intranet、Extranet 和 Internet)选择要实施的安全选项。

    学习 Web Services Enhancements 1.0 for Microsoft .NET (WSE)。

    学习如何使用代码访问安全来确保 .NET Framework 客户代码的安全。

    了解采取哪些措施来解决常见的 Web Services 威胁,这些威胁包括未授权访问、参数操纵、网络窃听、泄露配置数据以及消息重播。

    适用范围

    本模块适用于下列产品和技术:

    Microsoft Windows® 2000 Server 和 Microsoft Windows Server™ 2003

    Microsoft .NET Framework 1.1 和 ASP.NET 1.1

    如何使用本模块

    为了充分理解本模块内容,请:

    参阅模块 19 确保 ASP.NET 应用程序和 Web 服务的安全。该模块适合帮助管理员配置 ASP.NET Web 应用程序或 Web Services,使不太安全的应用程序变得安全。

    参阅模块 17 确保应用程序服务器的安全。参阅模块 17,以便熟悉远程应用程序服务器的注意事项。

    使用本指南中的“检查表”部分的检查表:保护 Web 服务的安全。该检查表总结了构建和配置安全的 Web Services 所需的安全措施。

    使用本模块可以了解消息级别的威胁以及如何防御这些威胁。

    并将应用程序类别作为一种解决常见问题的方法。本节将使用这些类别给出相关的信息。

    概述

    越来越多的公司使用 Web Services 通过 Internet 和企业 Extranet 向客户和商业伙伴提供产品和服务。这些服务提供商所需的安全要求非常高。在某些情况下(主要是在 Intranet 或 Extranet 情况下,在这两种情况下,您对两个终结点都有一定程度的控制权),可以使用操作系统和 Internet 信息服务 (IIS) 所提供的基于平台的安全服务来提供点对点的安全解决方案。然而,基于消息的 Web Services 体系结构和日益增加的跨信任边界的异构环境带来了新的挑战。这些情况需要在消息级别上解决安全问题,以支持跨平台的互操作性和通过多个中间节点进行路由。

    Web Services 安全 (WS-Security) 是用来解决这些问题的最新安全标准。Microsoft 已经发布了 Web Services Enhancements 1.0 for Microsoft .NET (WSE),以支持 WS-Security 和一组相关的最新标准。WSE 允许实施消息级别的安全解决方案,包括身份验证、加密和数字签名。

    注意:WSE 支持的规范和标准是不断变化的,因此当前的 WSE 并不保证与该产品将来的版本兼容。在写本文时,互操作性测试正在进行,使用的是由包括 IBM 和 VeriSign 在内的供应商所提供的非 Microsoft 工具包。

    威胁与对策

    要构建安全的 Web Services,需要了解相关的威胁。Web Services 面对的主要威胁是:

    未授权的访问

    参数操纵

    网络窃听

    配置数据的泄漏

    消息重播

    图 12.1 显示了 Web Services 面对的主要威胁和攻击。

    主要的 Web Services 威胁

    图 12.1
    主要的 Web Services 威胁

    未授权的访问

    提供敏感或限制性信息的 Web Services 应对其调用者进行身份验证和授权。攻击者可以利用弱的身份验证和授权机制,对敏感信息和操作进行未授权的访问。

    漏洞

    可导致通过 Web Services 进行未授权的访问的漏洞包括:

    未使用身份验证

    密码在 SOAP 头信息中以明文形式传递

    在未加密的通信通道中使用基本身份验证

    对策

    可以使用下列对策防止未授权的访问:

    在 SOAP 头信息中使用密码摘要进行身份验证。

    在 SOAP 头信息中使用 Kerberos 票证进行身份验证。

    在 SOAP 头信息中使用 X.509 证书进行身份验证。

    使用 Windows 身份验证。

    使用基于角色的授权来限制对 Web Services 的访问。通过使用 URL 授权来控制对 Web Services 文件 (.asmx) 的访问,或在 Web 方法级别通过使用主要权限需求,实现此目的。

    参数操纵

    参数操纵是指对 Web Services 客户与 Web Services 之间发送的数据进行未经授权的修改。例如,攻击者可以截获 Web Services 消息(例如,在通过中间节点到达目标的路由中),然后在将其发送到目标终结点前对其进行修改。

    漏洞

    可能用于参数操纵的漏洞包括:

    没有为防止篡改而对消息进行数字签名

    没有加密消息以提供隐私保护和防止篡改

    对策

    可以使用下列对策来防止参数操纵:

    对消息进行数字签名。数字签名用于收件人这一方,用来验证消息在传输过程中未被篡改。

    加密消息有效负载,以便提供隐私保护并防止篡改。

    网络窃听

    通过网络窃听,当 Web Services 消息在网络中传输时,攻击者可以查看这些消息。例如,攻击者可以使用网络监视软件检索 SOAP 消息中包含的敏感数据。其中有可能包括敏感的应用程序级别的数据或凭据信息。

    漏洞

    可以导致成功的网络窃听的漏洞包括:

    凭据在 SOAP 头信息中以明文形式传递

    没有使用消息级别的加密

    没有使用传输级别的加密

    对策

    可以使用下列对策来保护敏感的 SOAP 消息在网络中传递:

    使用传输级别的加密,如 SSL 或 IPSec。只有在两个终结点都可以控制的情况下,才能使用此对策。

    加密消息负载以提供隐私保护。当消息通过中间节点路由到最终目标时,可以使用此方法。

    配置数据的泄漏

    Web Services 配置数据的泄漏的方法主要有两种。第一种,Web Services 可能支持动态生成 Web Services 描述语言 (WSDL),或者可能在 Web 服务器上的可下载文件中提供 WSDL 信息。取决于具体情况,可能不需要此方法。

    注意:WSDL 描述 Web Services 的特征,例如,它的方法签名和支持的协议。

    第二种,如果异常处理不充分,Web Services 可能会泄漏对攻击者有用的敏感的内部实施详细信息。

    漏洞

    可以导致配置数据的泄漏的漏洞包括:

    可以不受限制地从 Web 服务器下载 WSDL 文件

    受限制的 Web Services 支持动态生成 WSDL,并允许未经授权的客户获得 Web Services 特性。

    弱的异常处理

    对策

    可以使用下列对策防止意外配置数据的泄漏:

    使用 NTFS 权限限制对 WSDL 文件的访问权。

    从 Web 服务器上删除 WSDL 文件。

    禁用文档协议以防动态生成 WSDL。

    捕获异常,并抛出 SoapException 或 SoapHeaderException,仅向客户端返回最少信息或无害信息。

    消息重播

    Web Services 消息可能会在传递过程中经过多个中间服务器。通过消息重播攻击,攻击者可以捕获并复制消息,并模拟客户端将其重播到 Web Services。消息可能被修改,也可能保持不变。

    漏洞

    可以导致消息重播的漏洞包括:

    消息未经加密

    消息未经数字签名以防止篡改

    由于没有使用唯一的消息 ID,因此无法检测重复的消息

    攻击

    最常见的消息重播攻击包括:

    简单重播攻击。攻击者捕获并复制消息,然后重播此消息,并冒充客户端。这种重播攻击无需恶意用户了解消息的内容。

    中间人攻击。攻击者捕获消息,然后更改部分内容(如送货地址),然后将其重播到 Web Services。

    对策

    可以使用下列对策解除消息重播的威胁:

    使用加密的通信通道,如 SSL。

    加密消息负载,以提供隐私保护并防止篡改。尽管这种方法不能防止简单的重播攻击,但它确实可以防止中间人攻击,避免了消息内容被修改后再重播。

    每个请求都应使用唯一的消息 ID 或 Nonce 来检测副本,并对消息进行数字签名以防篡改。

    注意:Nonce 是用于请求的、经过加密的唯一值。

    服务器响应客户端时,将发送唯一的 ID 并对该消息进行签名(包括此 ID)。当客户端产生其他请求时,将在消息中包含此 ID。服务器确保客户端发出的新请求中的 ID 与发送给客户端的前一个消息的 ID 是相同的。如果两者不同,则服务器将拒绝此请求,并假定正遭受重播攻击。

    攻击者无法欺骗此消息 ID,因为该消息是经过签名的。请注意,这种方法仅保护服务器免遭由客户端使用消息请求发起的重播攻击,但不为客户端提供针对重播响应的保护。

    设计注意事项

    准备开发 Web Services 之前,需要在设计阶段考虑许多问题。主要的安全注意事项包括:

    身份验证要求

    隐私和完整性要求

    资源访问标识

    代码访问安全

    身份验证要求

    如果 Web Services 提供了敏感或限制性信息,就需要对调用者进行身份验证以便授权。在 Windows 环境中,可以使用 Windows 身份验证。然而,如果无法同时控制两个终结点,则可以使用 WSE。WSE 提供了遵守最新 WS-Security 标准的身份验证解决方案。WSE 提供了一个标准框架,可以使用 SOAP 头信息以用户名和密码、Kerberos 票证、X.509 证书或自定义令牌来传递身份验证的详细信息。有关详细信息,请参阅本模块后面的身份验证部分。

    隐私和完整性要求

    如果在 Web Services 的请求或响应消息中传递敏感的应用程序数据,应考虑如何确保这些数据在传输过程中处于保密状态,并且没有被更改。WSE 通过数字签名提供了完整性检查,并且还支持 XML 加密,对整个消息负载的敏感元素进行加密。此方法的优点在于它是基于最新的 WS-Security 标准,并为通过多个中间节点传递的消息提供了一种解决方案。

    另外一种方法是使用通过 SSL 或 IPSec 通道的传输级别加密。该方案仅适用于可以同时控制两个终结点的情况。

    资源访问标识

    默认情况下,ASP.NET Web Services 不进行模拟,并使用权限最小的 ASPNET 进程帐户访问本地和远程资源。可以使用此 ASPNET 进程帐户访问远程网络资源,例如,可以通过在数据库服务器上创建本地帐户的镜像,来访问 SQL Server(需要 Windows 身份验证)。

    注意:在 Windows Server 2003 上,默认情况下使用 Network Service 帐户运行 Web Services。

    有关使用 ASP.NET 进程帐户远程访问数据库的详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web 服务的安全中的“数据访问”部分。

    如果使用模拟,Web 应用程序出现的各种问题和注意事项同样也出现于 Web Services。有关详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件和模块 19 确保 ASP.NET 应用程序和 Web 服务的安全中的“模拟”部分。

    代码访问安全

    以目标部署环境中的安全策略定义的信任级别为例。Web Services 的信任级别,是由该服务的 <trust> 元素配置定义的,可以影响该服务访问的资源类型和其他可执行的特权操作。

    同样,如果从 ASP.NET Web 应用程序调用 Web Services,则该 Web 应用程序的信任级别就决定了可以调用的 Web Services 的范围。例如,信任级别配置为中等信任的 Web 应用程序,在默认情况下只能调用本地计算机上的 Web Services。

    有关从中等信任或其他部分信任的 Web 应用程序调用 Web Services 的详细信息,请参阅模块 9 ASP.NET 代码访问安全性。

    输入验证

    如同所有接受数据输入的应用程序一样,Web Services 也必须验证所接收的数据,以便实施业务规则和防止可能的安全问题。标记为 WebMethod 属性的 Web 方法是 Web Services 的入口点。Web 方法可以接受强类型的输入参数或通常以字符串数据传递的松散类型的参数。这通常取决于设计 Web Services 的客户的范围和类型。

    强类型参数

    如果使用由 .NET Framework 类型系统描述的强类型参数(例如 integer、double、date)或其他自定义的对象类型(如 Address 或 Employee),则自动生成的 XML Schema Definition (XSD) 架构就会包含该数据的类型描述。客户可以使用该类型描述,在发送到 Web 方法的 SOAP 请求中,构造适当格式的 XML。然后 ASP.NET 使用 System.Xml.Serialization.XmlSerializer 类将传入的 SOAP 消息反序列化为公共语言运行库 (CLR) 对象。以下示例显示了一个 Web 方法,该方法可以接受包含内置数据类型的强类型输入。

    [WebMethod]
    public void CreateEmployee(string name, int age, decimal salary) {...}
    

    在上一个示例中,.NET Framework 类型系统自动进行类型检查。要验证 name 字段提供的字符的范围,可以使用正则表达式。例如,下列代码显示了如何使用 System.Text.RegularExpressions.Regex 类来限制输入字符的可能范围,并验证参数的长度。

    if (!Regex.IsMatch(name, @"^[a-zA-Z'.`-′\s]{1,40}$"))
    {
    // 无效的名称
    }
    

    有关正则表达式的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“输入验证”部分。下列示例显示了一个接受自定义的 Employee 数据类型的 Web 方法。

    using Employees;  // 自定义命名空间
    [WebMethod]
    public void CreateEmployee(Employee emp) { ...  }
    

    客户需要了解 XSD 架构以便能够调用 Web Services。如果该客户是一个 .NET Framework 客户端应用程序,则可以简单地传递一个 Employee 对象,如下所示:

    using Employees;
    Employee emp = new Employee();
    // 填充 Employee 字段
    // 向 Web Services 发送 Employee
    wsProxy.CreateEmployee(emp);
    

    对于不是基于 .NET Framework 的客户应用程序,必须根据负责此 Web Services 的组织所提供的架构定义,手动构造 XML 输入。

    这种强类型方法的优点是,.NET Framework 可以对输入数据进行解析,并根据类型定义验证这些数据。然而,您可能还需要在此 Web 方法内部对输入数据进行限制。例如,尽管类型系统确认 Employee 对象有效,然而可能还需要对 Employee 的各个字段进一步验证。您可能需要验证雇员的年龄是否大于 18 岁。还可能需要使用正则表达式,来限制 name 字段使用的字符的范围等等。

    有关限制输入的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“输入验证”部分。

    松散类型的参数

    如果使用字符串参数或字节数组传递任意数据,将会失去 .NET Framework 类型系统的许多优点。您必须手动解析输入数据,以便对其进行验证,因为自动生成的 WSDL 仅将这些参数简单描述为 xsd:string 类型的字符串输入。您需要以编程方式检查类型、长度、格式和范围,如下例所示。

    [WebMethod]
    public void SomeEmployeeFunction(string dateofBirth, string SSN)
    {
    . . .
    // 示例 1:对日期进行类型检查
    try
    {
    DateTime dt = DateTime.Parse(dateofBirth).Date;
    }
    // 如果类型转换失败就会抛出 FormatException 异常
    catch( FormatException ex )
    {
    // 无效的日期
    }
    // 示例 2:检查社会安全保险号的长度、格式和范围
    if( !Regex.IsMatch(empSSN,@"^\d{3}-\d{2}-\d{4}$",RegexOptions.None))
    {
    // 无效的社会安全保险号
    }
    }

    XML 数据

    在经典的商业对商业方案中,客户经常传递表示业务文档(如购货订单或销售发票)的 XML 数据。在对这些输入数据进行处理或将其传递到下游组件之前,必须通过 Web 方法以编程方式验证这些输入数据的有效性。

    客户端和服务器必须建立描述 XML 的架构,并达成一致。下列代码片段显示了 Web 方法如何使用 System.Xml.XmlValidatingReader 类来验证输入数据。本例中的输入数据描述了一个简单的图书订单。请注意,这些 XML 数据是通过简单的字符串参数传递的。

    using System.Xml;
    using System.Xml.Schema;
    [WebMethod]
    public void OrderBooks(string xmlBookData)
    {
    try
    {
    // 创建并加载一个验证的 reader
    XmlValidatingReader reader = new XmlValidatingReader(xmlBookData,
    XmlNodeType.Element,
    null);
    // 将 XSD 架构附加在此 reader 上
    reader.Schemas.Add("urn:bookstore-schema",
    @"http://localhost/WSBooks/bookschema.xsd");
    // 设置 XSD 架构的验证类型。
    // 也支持 XDR 架构和 DTD
    reader.ValidationType = ValidationType.Schema;
    // 创建并注册一个事件处理器,来处理验证错误
    reader.ValidationEventHandler += new ValidationEventHandler(
    ValidationErrors );
    // 处理输入数据
    while (reader.Read())
    {
    . . .
    }
    // 验证成功完成
    }
    catch
    {
    . . .
    }
    }
    // 验证错误事件处理器
    private static void ValidationErrors(object sender, ValidationEventArgs args)
    {
    // 从 args.Message 获得的错误详细信息
    . . .
    }
    

    下列代码片段显示了客户如何调用以上 Web 方法:

    string xmlBookData = "<book  xmlns='urn:bookstore-schema'
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>" +
    "<title>Building Secure ASP.NET Applications</title>" +
    "<isbn>0735618909</isbn>" +
    "<orderQuantity>1</orderQuantity>" +
    "</book>";
    BookStore.BookService bookService = new BookStore.BookService();
    bookService.OrderBooks(xmlBookData));
    

    以上示例使用了下列简单的 XSD 架构来验证输入数据。

    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="urn:bookstore-schema"
    elementFormDefault="qualified"
    targetNamespace="urn:bookstore-schema">
    <xsd:element name="book" type="bookData"/>
    <xsd:complexType name="bookData">
    <xsd:sequence>
    <xsd:element name="title" type="xsd:string" />
    <xsd:element name="isbn" type="xsd:integer" />
    <xsd:element name="orderQuantity" type="xsd:integer"/>
    </xsd:sequence>
    </xsd:complexType>
    </xsd:schema>
    

    下表显示了其他复杂的元素定义,它们可以用于 XSD 架构以便进一步限制各个 XML 元素。

    表 12.1:XSD 架构元素示例

    说明 示例

    使用正则表达式限制 XML 元素

    <xsd:element name="zip">
                <xsd:simpleType>
                <xsd:restriction base="xsd:string">
                <xsd:pattern value="\d{5}(-\d{4})?"  />
                </xsd:restriction>
                </xsd:simpleType>
                </xsd:element> 

    小数点后的数字限制为两位

    <xsd:element name="Salary">
                <xsd:simpleType>
                <xsd:restriction base="xsd:decimal">
                <xsd:fractionDigits value="2" />
                </xsd:restriction>
                </xsd:simpleType>
                </xsd:element> 

    限制输入字符串的长度

    <xsd:element name="FirstName">
                <xsd:simpleType>
                <xsd:restriction base="xsd:string">
                <xsd:maxLength value="50" />
                <xsd:minLength value="2" />
                </xsd:restriction>
                </xsd:simpleType>
                </xsd:element> 

    限制输入的值为枚举类型

    <xsd:element name="Gender">
                <xsd:simpleType>
                <xsd:restriction base="xsd:string">
                <xsd:enumeration value="Male" />
                <xsd:enumeration value="Female" />
                </xsd:restriction>
                </xsd:simpleType>
                </xsd:element> 

    有关详细信息,请参阅下列 Microsoft 知识库文章:

    307379,“How To:Validate an XML Document by Using DTD, XDR, or XSD in Visual C# .NET”(英文)。

    318504,“How To:Validate XML Fragments Against an XML Schema in Visual C#.NET”(英文)。

    SQL 注射

    “SQL 注射”允许攻击者使用 Web Services 的数据库登录在数据库中执行任意命令。如果 Web Services 使用输入数据来构造 SQL 查询,就可能存在 SQL 注射问题。如果 Web 方法访问此数据库,使用的应该是 SQL 参数,并且在理想情况下,是参数化的存储过程。SQL 参数会验证输入数据的类型和长度,并确保将其视为文本而不是可执行代码。有关 SQL 注射的所有对策的详细信息,请参阅模块 14 构建安全的数据访问中的“输入验证”部分。

    跨站点脚本

    通过跨站点脚本 (XSS),攻击者可以利用您的应用程序在客户端执行恶意脚本。如果从 Web 应用程序调用 Web Services,并将 Web Services 的输出结果以 HTML 数据流的形式发送回客户端,就有可能存在 XSS 问题。在这种情况下,应在 Web 应用程序中对从 Web Services 接收的输出结果进行编码,然后再将其返回给客户端。如果您不拥有此 Web Services,并且该服务在 Web 应用程序的信任边界之外,这样做尤其重要。有关 XSS 对策的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“输入验证”部分。

    身份验证

    如果 Web Services 输出敏感的、受限制的数据,或者提供受限制的服务,则需要对调用者进行身份验证。可用的身份验证方案有许多,大致可以分为以下三类:

    平台级别的身份验证

    消息级别的身份验证

    应用程序级别的身份验证

    平台级别的身份验证

    如果可以同时控制这两个终结点,并且它们在相同的域或信任域中,则可以使用 Windows 身份验证对调用者进行身份验证。

    基本身份验证

    可以使用 IIS 来配置 Web Services 的虚拟目录,以便进行基本身份验证。如果使用此方法,客户必须配置代理服务器,并提供用户名和密码形式的凭据。然后代理服务器将其与通过此代理服务器的每个 Web Services 请求一起传输。凭据是以明文形式传输的,因此应仅使用有 SSL 的基本身份验证。

    下列代码片段显示了 Web 应用程序如何提取最终用户提供的基本身份验证凭据,然后使用这些凭据调用 IIS 中的下游 Web Services 进行基本身份验证。

    // 检索客户端凭据(可用于基本身份验证)
    string pwd = Request.ServerVariables["AUTH_PASSWORD"];
    string uid = Request.ServerVariables["AUTH_USER"];
    // 设置凭据
    CredentialCache cache = new CredentialCache();
    cache.Add( new Uri(proxy.Url), // Web Services  URL
    "Basic",
    new NetworkCredential(uid, pwd, domain) );
    proxy.Credentials = cache;
    

    集成的 Windows 身份验证

    可以使用 IIS 来配置 Web Services 的虚拟目录,以便进行集成的 Windows 身份验证,从而根据客户端和服务器环境的不同进行 Kerberos 或 NTLM 身份验证。相对于基本身份验证,这种方法的优点是凭据不通过网络传递,从而排除了网络窃听的威胁。

    要调用配置为集成 Windows 身份验证的 Web Services,客户必须显式地配置代理服务器上的“凭据”属性。

    要以客户端的 Windows 安全上下文(来自模拟的线程令牌或进程令牌)覆盖 Web Services,可以将此 Web Services 代理的“凭据”属性设置为 CredentialCache.DefaultCredentials,如下所示。

    proxy.Credentials = System.Net.CredentialCache.DefaultCredentials;
    

    还可以使用如下所示的一组明确凭据:

    CredentialCache cache = new CredentialCache();
    cache.Add( new Uri(proxy.Url), // Web Services  URL
    "Negotiate",         // Kerberos 或 NTLM
    new NetworkCredential(userName, password, domain));
    proxy.Credentials = cache;
    

    如果需要指定明确凭据,请不要将其硬编码或存储到明文中。可以使用 DPAPI 加密帐户凭据,并将加密数据存储在 Web.config 的 <appSettings> 元素中或受限制的注册表项下。

    有关平台级别的身份验证的详细信息,请参阅“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:Authentication, Authorization, and Secure Communication”中的“Web Services Security”部分,其网址为:http://msdn.microsoft.com/library/en-us/dnnetsec/html/secnetlpMSDN.asp?frame=true(英文)。

    消息级别的身份验证

    可以使用 WSE 来实现符合最新 WS-Security 标准的消息级别的身份验证解决方案。此方法允许使用 SOAP 头信息以标准形式传递身份验证令牌。

    注意:如果双方同意使用 WS-Security,也必须就身份验证令牌的准确格式达成一致。

    WSE 可以使用和支持下列类型的身份验证令牌:

    用户名和密码

    Kerberos 票证

    X.509 证书

    用户名和密码

    可以在 SOAP 头信息中发送用户名和密码凭据。然而,由于它们是以明文发送的,存在网络窃听的威胁,因此该方法应与 SSL 一起使用。凭据是作为 SOAP 头信息中的 <Security> 元素的一部分发送的,如下所示。

    <wsse:Security
    xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wsse:UsernameToken>
    <wsse:Username>Bob</wsse:Username>
    <wsse:Password>YourStr0ngPassWord</wsse:Password>
    </wsse:UsernameToken>
    </wsse:Security>
    

    用户名和密码摘要

    您可以发送密码摘要,而不是发送明文密码。摘要是 Base64(UTF8 的编码 SHA1 哈希值)编码的密码。然而,除非在安全通道上使用此方法,否则拥有网络监视软件的攻击者仍然可以截获数据,并重新使用这些数据来非法访问您的 Web Services。要解决这种重播攻击的威胁,该摘要可以结合 Nonce 和创建时间戳一起使用。

    有 Nonce 和时间戳的用户名和密码摘要

    在此方法中,摘要是 Nonce 值、创建时间戳和密码的 SHA1 哈希。

    digest = SHA1(Nonce + creation timestamp + password)
    

    此方法中的 Web Services 必须维护一个包含 Nonce 值的表,并拒绝包含重复 Nonce 值的任何消息。尽管此方法有助于保护密码,并为防止重播攻击提供了基础,但在计算过期日期时,客户与提供商之间会出现时钟同步的问题,而且它不能防止攻击者捕获消息、修改 Nonce 值,然后将此消息重播给 Web Services。要解决此威胁,必须对消息进行数字签名。通过 WSE,可以使用自定义的令牌或 X.509 证书对消息进行签名。此方法可以基于公钥/私钥对来防止篡改和提供身份验证。

    Kerberos 票证

    可以发送包含 Kerberos 票证的安全令牌,如下所示。

    <wsse:Security
    xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wsse:BinarySecurityToken
    ValueType="wsse:Kerberosv5ST"
    EncodingType="wsse:Base64Binary">
    U87GGH91TT ...
    </wsse:BinarySecurityToken>
    </wsse:Security>
    

    X.509 证书

    还可以发送一个作为身份验证令牌的 X.509 证书来提供身份验证。

    <wsse:Security
    xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wsse:BinarySecurityToken
    ValueType="wsse:X509v3"
    EncodingType="wsse:Base64Binary">
    Hg6GHjis1 ...
    </wsse:BinarySecurityToken>
    </wsse:Security>
    

    有关以上方法的详细信息,请参阅 WSE 附带的示例。

    应用程序级别的身份验证

    通过让应用程序使用自定义的 SOAP 头信息,可以设计和构建您自己的自定义身份验证。在此之前,应检查平台和 WSE 提供的功能,以查看这些功能是否可用。如果必须使用自定义身份验证机制,而且需要加密,可以使用 System.Security.Cryptography 命名空间提供的标准加密算法。

    授权

    身份验证以后,根据调用者的身份或角色成员关系,可以将其限制在 Web Services 提供的部分功能内。可以限制对服务终结点(在 .asmx 文件级别上)、各个 Web 方法或 Web 方法内的特定功能的访问。

    Web Services 终结点身份验证

    如果将 Web Services 配置成集成的 Windows 身份验证,则可以基于最初的调用者的安全上下文,在 Web Services (.asmx) 的文件上配置 NTFS 权限来控制访问。这种授权是由 ASP.NET 的 FileAuthorizationModule 执行的,而且不需要模拟。

    不管哪种类型的身份验证,都可以使用 ASP.NET 的 UrlAuthorizationModule 来控制对 Web Services (.asmx) 文件的访问。通过将 <allow> 和 <deny> 元素添加到 Machine.config 或 Web.config 中的 <authorization> 元素中,可以实现此配置。

    有关这两种形式授权的详细信息,请参阅模块 19 确保 ASP.NET 应用程序和 Web 服务的安全中的“授权”部分。

    Web 方法授权

    可以根据调用者的身份或角色成员关系,使用说明性的主要权限需求来控制对每个 Web 方法的访问。调用者的身份和角色成员关系是由与当前的 Web 请求(通过 HttpContext.User 来访问)相关的主要对象来维护。

    [PrincipalPermission(SecurityAction.Demand, Role=@"Manager")]
    [WebMethod]
    public string QueryEmployeeDetails(string empID)
    {
    }
    

    有关主要权限需求的详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“授权”部分。

    编程授权

    通过调用 Web 方法中的 IPrincipal.IsInRole,可以使用命令性的权限检查或明确的角色检查,进行精确的授权逻辑,如下所示。

    // 在此假设使用非 Windows 身份验证。使用 Windows 身份验证
    // 将 User 对象转换为 WindowsPrincipal 对象并使用 Windows 组作为
    // 角色名
    GenericPrincipal user = User as GenericPrincipal;
    if (null != user)
    {
    if ( user.IsInRole(@"Manager") )
    {
    // 授予用户执行管理功能
    }
    }

    敏感数据

    如果 Web Services 的请求或响应消息用于传递敏感的应用程序数据,如信用卡号、雇员详细信息等,则必须解决在中间应用程序节点上存在的网络窃听或信息泄漏的威胁。

    在封闭的环境中,可以同时控制两个终结点,此时可以使用 SSL 或 IPSec 来提供传输层加密。在其他环境中,如果消息是通过中间应用程序节点路由的,则需要消息级别的解决方案。根据 WWW 联合会 (W3C) XML 加密标准,WS-Security 标准定义了一个机密性服务,允许在传输 SOAP 头信息之前对其进行部分或全部加密。

    XML 加密

    可以使用以下三种方法全部或部分加密 SOAP 消息:

    使用 X.509 证书的非对称加密

    使用共享密钥的对称加密

    使用自定义二进制令牌的对称加密

    使用 X.509 证书的非对称加密

    通过此方法,客户可以使用 X.509 证书的公钥部分来加密 SOAP 消息。这种加密只能由拥有相应私钥的服务来解密。

    Web Services 必须能够访问相关的私钥。默认情况下,WSE 在本地计算机的存储区中搜索 X.509 证书。可以使用 Web.config 中的 <x509> 配置元素将存储位置设置为当前的用户存储区,如下所示。

    <configuration>
    <microsoft.web.services>
    <security>
    <x509 storeLocation="CurrentUser" />
    </security>
    </microsoft.web.services>
    </configuration>
    

    如果使用用户存储区,则必须加载 Web Services 的进程帐户的用户配置文件。如果使用默认的 ASPNET(权限最小的本地帐户) 运行 Web Services,则 .NET Framework 版本 1.1 将加载此帐户的用户配置文件,从而访问此用户密钥的存储区。

    对于使用 .NET Framework 版本 1.0 构建的 Web Services,则不会加载 ASPNET 用户配置文件。在这种情况下,有以下两种选择。

    使用自定义的、权限最小的帐户(先前交互登录到 Web 服务器时所用的帐户)运行 Web Services,并创建一个用户配置文件。

    将此密钥存储在本地计算机的存储区中,并向 Web Services 进程帐户授予访问权。在 Windows 2000 上,默认情况下,此帐户是 ASPNET 帐户。默认情况下,在 Windows Server 2003 上此帐户是 Network Service 帐户。

    要授予访问权,可以使用 Windows 资源管理器在以下文件夹上配置 ACL,向 Web Services 进程帐户授予完全控制权限。

    \Documents and Settings\All Users\Application Data\
                Microsoft\Crypto\RSA\MachineKeys
                

    有关详细信息,请参阅 WSE 文档中的“管理 X.509 证书”、“使用 X.509 证书加密 SOAP 消息”以及“使用 X.509 证书解密 SOAP 消息”部分。

    使用共享密钥的对称加密

    使用对称加密,Web Services 及其客户可以共享一个密钥来加密和解密 SOAP 消息。尽管客户和服务提供商必须使用某些“不合适”的机制来共享密钥,不过这种加密比非对称加密速度快。

    有关详细信息,请参阅 WSE 文档中的“使用共享密钥加密 SOAP 消息”和“使用共享密钥解密 SOAP 消息”部分。

    使用自定义二进制令牌的对称加密

    您还可以使用 WSE 定义一个自定义的二进制令牌,来封装用于加密和解密消息的自定义安全凭据。此时,代码需要以下两个类。sender 类必须从 BinarySecurityToken 类派生,以便封装自定义安全凭据,并加密消息。recipient 类必须从 DecryptionkeyProvider 类派生,以便检索密钥并解密消息。

    有关详细信息,请参阅 WSE 文档中的“使用自定义二进制安全令牌加密 SOAP 消息”和“使用自定义二进制安全令牌解密 SOAP 消息”部分。

    加密部分消息

    默认情况下,WSE 加密整个 SOAP 正文,而且不加密 SOAP 头信息。然而,还可以使用 WSE 以编程方式加密和解密部分消息。

    有关详细信息,请参阅 WSE 文档中的“指定要签名或加密的部分 SOAP 消息”部分。

    参数操纵

    Web Services 的参数操纵是一种威胁,当消息请求或响应在客户与服务之间传递时,攻击者可以使用某种方法更改消息负载。

    要解决此威胁,可以对 SOAP 消息进行数字签名,允许消息收件人通过密码来验证此消息自接受签名以来是否被更改过。有关详细信息,请参阅 WSE 文档中的“对 SOAP 消息进行数字签名”部分。

    异常管理

    返回给客户的异常详细信息只能包含最低限度的信息,而且不应泄漏任何内部实施细节。例如,下面是一个已经允许传播给客户的系统异常示例。

    System.Exception:User not in managers role(用户不是管理员角色)
    at EmployeeService.employee.GiveBonus(Int32 empID, Int32 percentage) in
    c:\inetpub\wwwroot\employeesystem\employee.asmx.cs:line 207
    

    上述异常的详细信息向该服务的客户泄漏了目录结构和其他详细信息。恶意用户可以使用这些信息获得虚拟目录的路径,以便进一步攻击。

    Web Services 可以抛出以下三种类型的异常:

    SoapException 对象。
    这些异常可以由 CLR 或 Web 方法的执行代码生成。

    SoapHeaderException 对象。
    当服务无法正确处理客户发送的 SOAP 请求时,就会自动生成此异常。

    Exception 对象。
    Web Services 可以抛出从 System.Exception 派生的自定义的异常类型。准确的异常类型是根据错误条件来指定的。例如,它可能是 .NET Framework 标准异常类型中的一个,如 DivideByZeroException 或 ArgumentOutOfRangeException 等等。

    不管哪种异常类型,异常的详细信息都使用标准的 SOAP <Fault> 元素传播到客户端。客户端和由 ASP.NET 创建的 Web Services 并不直接解析 <Fault> 元素,而是通常用 SoapException 对象来处理。这允许客户端设置 try 块,来捕获 SoapException 对象。

    注意:如果从自定义的 HTTP 模块中抛出 SoapException,则该异常不会自动序列化为 SOAP <Fault> 元素。在这种情况下,必须手动创建 SOAP <Fault> 元素。

    使用 SoapException

    下列代码显示了一个简单的 WebMethod,由于应用程序逻辑验证失败,因而生成异常。发送到客户端的错误信息已降到最少。在此示例中,为客户端提供了可以用来寻求支持的帮助台参考信息。在 Web 服务器上,供帮助台参考的详细错误描述被记录下来,以便帮助支持人员诊断问题。

    using System.Xml;
    using System.Security.Principal;
    [WebMethod]
    public void GiveBonus(int empID, int percentage)
    {
    // 仅管理人员可以发放奖金
    // 本示例使用 Windows 身份验证
    WindowsPrincipal wp = (HttpContext.Current.User as WindowsPrincipal);
    if( wp.IsInRole(@"Domain\Managers"))
    {
    // 授予用户发放奖金的权限
    . . .
    }
    else
    {
    // 在服务器上记录错误详细信息。例如:
    // DOMAIN\Bob 试图为 ID 为 345667 的雇员发放奖金。
    // 由于 DOMAIN\Bob 不是管理人员,因此拒绝访问。
    // 注意:用户名可以从 wp.Identity.Name 获得
    // 使用 SoapException 向客户端返回最少的错误信息
    XmlDocument doc = new XmlDocument();
    XmlNode detail = doc.CreateNode(XmlNodeType.Element,
    SoapException.DetailElementName.Name,
    SoapException.DetailElementName.Namespace);
    // 这是此异常的详细部分
    detail.InnerText = "User not authorized to perform requested operation";
    throw new SoapException("Message string from your Web service(来自您的 Web Services 的消息字符串)",
    SoapException.ServerFaultCode,
    Context.Request.Url.AbsoluteUri, detail, null );
    }
    }
    

    用于处理可能的 SoapException 的客户代码如下:

    try
    {
    EmployeeService service = new EmployeeService();
    Service.GiveBonus(empID,percentage);
    }
    catch (System.Web.Services.Protocols.SoapException se)
    {
    // 从 se.Detail.InnerText 提取自定义消息
    Console.WriteLine("Server threw a soap exception" + se.Detail.InnerText );
    }

    Global.asax 中的应用程序级别的错误处理

    ASP.NET Web 应用程序通常在 Global.asax 的 Application_Error 事件处理程序中处理那些被允许越过方法边界传播的应用程序级异常。此功能并不适用于 Web Services,因为 Web Services 的 HttpHandler 在异常到达其他处理程序之前已经将其捕获。

    如果需要应用程序级别的异常处理,可以创建自定义的 SOAP 扩展来处理它。有关详细信息,请参阅 Microsoft® .NET Framework SDK 1.1 版的“Building Applications”部分中的 MSDN 文章:Altering the SOAP Message using SOAP Extensions。

    审核与日志记录

    通过 Web Services,可以使用平台级别的功能或在 Web 方法实现中使用自定义代码,来审核和记录行为的详细信息和事务。

    可以使用 System.Diagnostics.EventLog 类来编写代码,将操作记录到 Windows 事件日志中。从 Web Services 使用此类的权限要求和技术与 Web 应用程序的权限要求和技术相同。有关详细信息,请参阅模块 10 构建安全的 ASP.NET 网页和控件中的“审核与日志记录”部分。

    代理服务器的注意事项

    如果使用 WSDL 自动生成一个代理服务器类以便与 Web Services 通信,应验证生成的代码和服务终结点,以确保与所需的 Web Services 通信,而不是与欺骗性服务通信。如果远程服务器上的 WSDL 文件保护不当,恶意用户就有可能篡改此文件,并更改终结点的地址,从而影响生成的代理服务器代码。

    尤其应检查 .wsdl 文件中的 <soap:address> 元素,并验证其是否指向所希望的位置。如果使用 Visual Studio .NET,并通过使用“添加 Web 引用”对话框来添加 Web 引用,应向下滚动并检查服务终结点。

    最后,不管是使用 Visual Studio.NET 来添加 Web 引用,还是使用 Wsdl.exe 手动生成代理服务器代码,都应仔细检查代理服务器代码并查找任何可疑代码。

    注意:可以将 Web Services 代理的“URL 行为”属性设置为“动态”,从而可以在 Web.config 中指定终结点的地址。

    代码访问安全的注意事项

    代码访问安全可以限制所访问的资源和由 Web Services 代码执行的操作。ASP.NET Web Services 服从于由 Web Services 的 <trust> 元素所配置的 ASP.NET 代码访问安全策略。

    代码访问安全策略必须将 WebPermission 权限授予调用 Web Services 的 .NET Framework 客户代码。WebPermission 权限的准确状态决定了可以调用的 Web Services 的范围。例如,可以对代码进行限制,使其只能调用本地 Web Services 或指定服务器上的服务。

    如果完全信任客户代码,则可以授予其不受限制的 WebPermission 权限,从而可以调用任意 Web Services。部分信任的客户代码受到下列限制:

    如果从中等信任的 Web 应用程序调用 Web Services,默认情况下只能访问本地 Web Services。

    对于使用 WSE 类的客户代码,必须授予其完全信任。例如,如果 Web Services 代理类是从由 WSE 提供的 Microsoft.Web.Services.WebServicesClientProtocol 派生的,则需要完全信任它。要从部分信任的 Web 应用程序使用 WSE,必须使用沙箱机制调用 Web Services。

    有关从部分信任的 Web 应用程序调用 Web Services 的详细信息,请参阅模块 9 ASP.NET 代码访问安全性。有关 WebPermission 的详细信息,请参阅模块 8 代码访问安全的实践中的“Web 服务”部分。

    部署注意事项

    可用的安全选项的范围主要取决于 Web Services 试图包含的特定部署方案。如果构建的应用程序在 Intranet 内使用 Web Services,则可以随意使用最多的安全选项和技术。然而,如果 Web Services 可以通过 Internet 公开访问,则可以使用的选项就受到很大限制。本节描述了各种部署方案的适用性的含义,这些方法可以保护本模块先前讨论的 Web Services 的安全。

    Intranet 部署

    由于可以控制客户应用程序、服务和平台,Intranet 通常可以提供最多的可用选项以保护 Web Services 的安全。

    在 Intranet 情况下,通常可以从整个身份验证和安全通信选项中选择。例如,如果客户和服务在相同域或信任域中,则可以考虑使用 Windows 身份验证。您可以指定客户端应用程序开发人员设置客户端代理服务器的凭据属性,将用户的 Windows 凭据发送到 Web Services。

    Intranet 通信通常在专用网络上进行,具有一定的安全性。如果这样还不满足,可以考虑使用 SSL 对通信进行加密。还可以使用消息级别的安全,并在客户端和服务器上都安装 WSE,在这两个终结点上对应用程序透明地处理安全事务。WSE 支持身份验证、数字签名和加密。

    Extranet 部署

    在 Extranet 情况下,可能需要通过 Internet 将 Web Services 提供给数量有限的合作伙伴。尽管被管理的客户端应用程序来自分散的、独立的环境,用户团体仍然可以了解、预测并且可能使用它们。在这种情况下,需要一种适合双方的、并且不依赖于受信任域的身份验证机制。

    如果使帐户信息对双方都可用,则可以使用基本身份验证。如果使用基本身份验证,应确保使用 SSL 来保护凭据的安全。

    注意:SSL 仅保护通过网络的凭据。如果恶意用户在客户端计算机上成功安装代理工具(如 sslproxy)以截获调用信息,然后通过 SSL 将其转发到 Web Services,此时 SSL 并不保护这些凭据。

    Extranet 可以使用的另外一个选项是,使用 IIS 客户端证书身份验证,而不是传递显式凭据。在这种情况下,调用方应用程序在调用时必须携带有效的证书。Web Services 使用此证书对调用者进行身份验证,并对此操作进行授权。有关详细信息,请参阅 MSDN 文章“Building Secure ASP.NET Applications”中的“Extranet Security”部分,其网址为:http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetch06.asp(英文)。

    Internet 部署

    如果为大量 Internet 客户提供 Web Services,并且需要进行身份验证,则可用的选项非常有限。由于客户无法将其凭据映射到合适的域帐户,因此任何形式的平台级别身份验证都不合适。如果目标 IIS Web 服务器或其前面的 ISA 服务器必须知道大量客户端证书,则使用 IIS 客户端证书身份验证和传输 (SSL) 级别的身份验证也会出现问题。此时最合适的选择只剩下消息和应用程序级别的身份验证和授权。由服务的客户所传递的凭据是以用户名、密码、证书、Kerberos 票证或自定义令牌的形式传递的,可以由 Web Services 基础结构 (WSE) 透明地进行验证,或在目标服务内以编程方式进行验证。客户端证书很难大规模管理。密钥管理(包括发出和吊销)也会出现问题。同时,基于证书的身份验证占用大量资源,因此有大量客户端时,其伸缩性就成为问题。

    SSL 通常只加密服务器端证书的网络通信,不过还可以通过消息级别的加密来弥补。

    从安全的角度来说,使用客户端证书有其优点,但当有大量用户时,通常就会出现问题。您必须仔细管理证书,并考虑如何将其传送给客户端、续订、吊销等问题。Internet 情况下的另一个可能问题是该解决方案的整体可伸缩性。对于大规模的 Web Services 来说,其工作量非常大,因而操作开销或加密/解密和证书验证可能导致问题。

    小结

    WS-Security 是 Web Services 安全的最新标准。规范中定义了身份验证的选项,可以通过使用 SOAP 头信息以标准方法传递安全令牌。令牌可以包括用户名和密码凭据、Kerberos 票证、X.509 证书或自定义令牌。WS-Security 还可以解决消息的隐私和完整性问题。您可以加密整个或部分消息以提供隐私保护,并对其进行数字签名以提供完整性。

    在 Intranet 环境中,由于可以同时控制两个终结点,因而可以使用平台级别的安全选项,如 Windows 身份验证。对于更复杂的环境,如果没有同时控制两个终结点,并且消息要通过中间应用程序节点进行路由,则需要消息级别的解决方案。下面的“其他参考资料”部分列出了一些网站,可以用来查找最新的 WS-Security 标准及其相关的 WSE 工具包。这些工具包允许构建符合此标准和其他最新 Web Services 标准的解决方案。

    其他资源

    有关详细信息,请参阅下列资源:

    有关可打印的检查表,请参阅本指南的“检查表”部分中的检查表:保护 Web 服务的安全。

    您可以从 Microsoft Web Services Developer Center 的主页下载 WSE,其网址为:http://msdn.microsoft.com/webservices(英文)。

    有关 Web Services 的身份验证、授权和安全通信的详细信息,请参阅“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:Authentication, Authorization, and Secure Communication”中的“Web Services Security”部分,其网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetch10.asp(英文)。

    有关特定于 Web Services 安全的文章,请参阅以下 MSDN 文章,其网址为:http://msdn.microsoft.com/webservices/building/security/default.aspx(英文)。

    有关特定于 Web Services 增强的文章,请参阅以下 MSDN 文章,其网址为:http://msdn.microsoft.com/webservices/building/wse/default.aspx(英文)。

    有关使用 SSL 和 Web Services 的详细信息,请参阅“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:Authentication, Authorization, and Secure Communication”中的“How To”部分中的“How to Call a Web Service Using SSL”,其网址为:http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetHT14.asp(英文)。

    有关使用客户端证书和 Web Services 的详细信息,请参阅“Microsoft patterns & practices Volume I, Building Secure ASP.NET Web Applications:Authentication, Authorization, and Secure Communication”中的“How To”部分中的 MSDN 文章“How To:Call a Web Service Using Client Certificates from ASP.NET”,其网址为:http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetHT13.asp(英文)。

    有关 WS-Security 的详细信息,请参阅 MSDN 文章“WS-Security:New Technologies Help You Make Your Web Services More Secure”,其网址为:http://msdn.microsoft.com/msdnmag/issues/03/04/WS-Security/default.aspx(英文)。

    有关 XML 加密的详细信息,请参阅 W3C XML Encryption Working Group,其网址为:http://www.w3.org/Encryption/2001/(英文)。

  • 相关阅读:
    [帮助文档] [SageMath Thematic Tutorial] Chapter 10 使用Sagemath进行数值计算
    [转]dd命令、cp命令详解+dd命令、cp命令对比 delong
    MBR内容解析
    记一个编译错误:命名冲突、宏定义、头文件包含顺序
    防止STL容器迭代器失效
    日历时间
    观点
    编码规范
    《转》impress.js页面PPT
    <转>Spring Test 整合 JUnit 4 使用总结
  • 原文地址:https://www.cnblogs.com/wzyexf/p/407552.html
Copyright © 2011-2022 走看看