65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 PHP 编写增强的 SOAP-Web 服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (11投票s)

2012 年 4 月 9 日

CDDL

11分钟阅读

viewsIcon

59207

downloadIcon

2489

关于如何使用 PHP 编写增强的 PHP-SOAP-webservices( 带自动 WSDL 生成)的教程

这是一个关于如何使用 PHP 编写增强型 SOAP Web 服务的教程。

引言

当需要将业务逻辑集中共享时,PHP 与 SOAP 结合非常有用。本文介绍了如何轻松地用 PHP 编写 Web 服务,以及如何使用 C# 访问它们。代码保持简单,即使是中级初学者也能理解如何编写 Web 服务,同时又足够灵活,高级开发人员也能使用。

由于默认的 SoapServer 类缺少一些功能,我编写了一个框架来支持自动 WSDL 生成或传输大数据等功能,我将一步一步地解释。 

定义服务

服务不过是任何人都可以请求执行预定义任务并返回结果的“东西”。

服务可以通过对象来实现,但请记住,服务是过程式的,而不是面向对象的。因此,所谓的“东西”是一个对象,任务对应方法和参数,结果是方法的返回值。

重要的是 Web 服务定义清晰,以便客户端确切地知道如何使用这些服务和方法。为了描述 Web Service Description,已建立了一种基于 XML 的 Language:WSDL

WSDL 生成

手动编写 WSDL 文档会导致代码冗余,因为 PHP 中的每个 Web 服务操作都必须在 WSDL 中指定。更改 Web 服务方法的签名时,必须重写相应的 WSDL 部分。那么为什么不让 PHP 本身来生成它呢?

PHP 中的类型问题

WSDL 要求使用 XSD(XML Schema)描述所有使用的参数和结果类型。但在 PHP 中,变量的类型不需要指定,因此默认情况下,如果程序员不知道,就没有办法确定方法中使用的类型。因此,必须以 WSDL 构建器能够理解的方式为 Web 服务添加注释。

本文提供的框架需要以下注释语法来提取类型信息。对于方法

/**
 * @param ComplexNumber the first summand
 * @param ComplexNumber the second summand
 * @return ComplexNumber the sum of a and b
 * @access web
 */
function AddTwoComplexNumbers($a, $b) { [...] }

以及对于结构

/**
 * @name Complex
 */
class ComplexNumber
{
  /**
   * The real part
   * @var float
   * @access web
   */
  public $Real;
  /**
   * The imaginary part
   * @var float
   * @access web
   */
  public $Imaginary;
}

@access 参数用于确定元素是否可从外部访问 - 没有此注释的元素将不会发布(出于安全原因,未标记的方法无法从外部调用)。@name 参数是可选的,它指定用于 WSDL 生成的名称。类型声明后的可选注释,用空格分隔,将包含在 WSDL 文档中。

使用框架的 WsdlBuilder 类

当要生成 WSDL 文档的服务按照上述示例进行注释时,可以使用 WebserviceInfo 类,否则必须编写一个兼容的类。

以下列表显示了如何使用 WsdlBuilder 类。

class TestWebservice
{
  //include method AddTwoComplexNumbers from example above
}

//include class ComplexNumber from example above

require_once "webserviceInfo.php";

WebserviceRegistry::RegisterWebservice
    ("Test", "TestWebservice"); //"TestWebservice" is the class name of the service
    //"Test" is the actual name used for WSDL-generation

require_once "wsdlBuilder.php";
$builder = new WsdlBuilder
    ("http://www.example.com/myservice"); 	//The URI is the target namespace 
					//of the WSDL-document

require_once "functions.php"; //For GetServerUrl

foreach (WebserviceRegistry::ListWebserviceInfos() 
		as $info) //Add all registered Webservices
  $builder->AddService($info, GetServerUrl() . $_SERVER["SCRIPT_NAME"] . 
		"/service/" . $info->serviceName);
    //The second parameter is the full location where the service can be accessed

$doc = $builder->CreateDocument(); //Create DOM-Document
file_put_contents("services.wsdl", $doc->saveXml());

或者,可以在 wsdlBuilder.php 中使用 WsdlFile

require_once "wsdlBuilder.php";

$urlFormat = GetServerUrl() . $_SERVER["SCRIPT_NAME"] . "/service/%s";
  //%s will be replaced with the service name

$wsdlFile = new WsdlFile(WebserviceRegistry::ListWebserviceInfos(), 
		"http://www.example.com/services", $urlFormat);
$wsdlFile->FileName = "services.wsdl";
$wsdlFile->IsFileCached = false;

$wsdlFile->GetFileName() 方法将在需要时创建文件(取决于属性 $wsdlFile->IsFileCached,如果为 true,则仅在文件不存在时创建;如果为 false,则始终创建文件),并返回其文件名。

SoapWsdlResponse 和 SoapWebserviceResponse

为了简化开发,soapResponses.php 中的 SoapWsdlResponseSoapWebserviceResponse 类通过调用它们的 Load() 方法来处理整个 SOAP 响应。

SoapWsdlResponse 类将 WsdlFile 实例作为构造函数参数,并在调用 Load() 时将 WSDL 文档发送给客户端。

SoapWebserviceResponse 类也以 WsdlFile 实例作为构造函数参数,但此外还需要传递应该处理请求的服务。调用 Load() 时,所有其他工作,如检查是否生成 WSDL 文件或创建代理(见下一章),都会自动完成。

以下示例显示了如何使用这些类

if ($query === "/services.wsdl") //WSDL-Retrival
	$response = new SoapWsdlResponse($wsdlFile); 
	//$wsdlFile is the WsdlFile-instance from the last example
else if (startsWith($query, "/service/")) //SOAP-Call
{
	$serviceName = substr($query, strlen("/service/"));
	$service = WebserviceRegistry::GetWebservice($serviceName);
	$response = new SoapWebserviceResponse($wsdlFile, $service);
	$response->WsdlCacheEnabled = false;
}

if ($response != null)
	$response->Load(); //Sends the response to the client

WsdlBuilder 如何映射类型到 XSD

WsdlBuilder 在内部使用 IWsdlTypeMapping 接口将类型映射到 URI。

该接口非常简单

interface IWsdlTypeMapping
{
  /**
   * @param WebserviceType $type
   * @param IWsdlTypeMappingContext $context
   * @return string Namespace:Element
   */
  public function GetTypeMapping(WebserviceType $type, IWsdlTypeMappingContext $context);
}

提供的接口 IWsdlTypeMappingContext 看起来像

interface IWsdlTypeMappingContext
{
  /**
   * @return DOMDocument
   */
  public function GetDoc();
  public function AppendToSchema(DOMElement $element);
  /**
   * @param WebserviceType $type
   * @return string Namespace:Element
   */
  public function GetTypeMapping(WebserviceType $type);
}

其中 AppendToSchema 将自定义 DOMElement 添加到 WSDL 文档的 Schema 节点。可以通过使用 GetDoc 方法获取新的 DOMElement,该方法返回主 DOMDocumentGetTypeMapping 方法允许询问其他类型映射器是否可以映射子类型(此时会将类型添加到文档中),这在映射多维数组时是必需的。请确保这不会导致无限递归。

实现了 IWsdlTypeMapping 接口的对象可以如下添加到 WsdlBuilder

WsdlBuilder::$WsdlTypeMappings[] = new MyCustomTypeMapping();

自定义类型映射器能够改变客户端看到的参数的呈现方式。通过返回常量 WsdlBuilder::NoMapping,甚至可以在 WSDL 描述中隐藏参数,结合下一章,这是一个非常有趣的选择。

通过代理进行预处理和后处理

Proxy

为了避免 SoapServer 的缺点,可以使用代理来编辑值,然后再将其作为参数传递给调用的方法,并在返回结果后但在发送给客户端之前。这提供了增强的功能,例如,DateTime 对象在作为结果传递时可以转换为 SOAP 客户端理解的表示形式。

使用 WebserviceProxy 类

从技术上讲,代理拦截 SOAP 服务器和服务对象之间的控制流。

require_once "webserviceProxy.php";
$server->setObject(new WebserviceProxy($service));

从服务器到代理的调用被重定向到服务。

代理将客户端的值转换回 PHP 原生对象(例如 DateTime 值),并将服务的值转换为客户端可理解的表示形式。

自定义转换器

要编写自定义转换器,只需实现 IObjectConverter 接口

interface IObjectConverter
{
  /**
   * @param WebserviceType $objectType the type of the target value
   * @param mixed $message the message from client to decode
   * @param IObjectConvertContext $context
   * @return mixed the decoded value
   */
  public function ConvertBack
	(WebserviceType $targetType, $message, IObjectConvertBackContext $context);

  /**
   * @param WebserviceType $objectType the type of $object
   * @param mixed $object the source from service to convert
   * @param IObjectConvertContext $context
   * @return string the converted message
   */
  public function Convert(WebserviceType $objectType, 
		$object, IObjectConvertContext $context);
}

上下文看起来像

interface IObjectConvertContext
{
  public function Convert(WebserviceType $targetType, $object);
}

interface IObjectConvertBackContext
{
  public function ConvertBack(WebserviceType $targetType, $message);
}

这允许询问其他转换器,这在处理嵌套类型时很重要。

同样,请确保这不会导致无限递归。

服务注册

如果提供多个服务,则需要进行组织。为此,static ServiceRegistry 类负责。

它的使用非常简单

require_once "webserviceInfo.php";

require_once "test.php";
WebserviceRegistry::RegisterWebservice
	("Test", "TestWebservice"); //Register a webservice-class

$service = WebserviceRegistry::GetWebservice("Test"); //Instantiates the webservice-class

WebserviceRegistry::ListWebserviceInfos(); //Returns WebserviceInfo[]

身份验证

有几种方法可以实现身份验证。我推荐使用 Http Basic 身份验证,因为它非常简单,并且与 WCF(Windows Communication Foundation)配合得很好,所以我将首先解释它。但是,由于并非所有 PHP 服务器(例如 CGI)都支持此身份验证模式,因此我也会解释 Http Header 身份验证。由于我自己的网站不支持 Http Basic 身份验证,并且自定义标头的身份验证更复杂,因此附件示例显示了如何使用 Http Header 身份验证。

Http Basic 身份验证

对于 Http Basic 身份验证,客户端在每次请求时都会发送两条信息:用户名,它将存储在 $_SERVER["PHP_AUTH_USER"] 中,密码,它将存储在 $_SERVER["PHP_AUTH_PW"] 中。它们应该在处理 SOAP 请求之前或期间进行检查。

要指示凭据丢失或错误(或通常需要身份验证),可以发送以下标头

header('WWW-Authenticate: Basic realm="MyRealm"');
header("HTTP/1.0 401 Unauthorized");

领域描述了受保护的部分,可以自由选择。

不幸的是,用户名和密码都以纯文本形式发送,所以如果需要加密,则应使用 SSL。
使用 Http Basic 身份验证时,WCF 要求启用 SSL。

Http Header 身份验证

此身份验证类似于 Http Basic 身份验证:客户端在每次调用时都将用户名和密码作为标头信息发送。但相反,在客户端,发送标头信息必须手动完成,服务器无需使用“WWW-Authenticate”来指示需要身份验证。

标头名称没有默认名称,但应称为“USER”和“PASSWORD”,以便服务器可以使用 $_SERVER["HTTP_USER"]$_SERVER["HTTP_PASSWORD"] 进行检查。

即使是 CGI-php 服务器也支持此方法,并且 WCF 不需要启用 SSL(但尽管如此,密码交换也应该被加密)。 

如何传输大数据

为了传输二进制数据或包含非法字符的数据,SOAP 提供了 DIME(Direct Internet Message Encapsulation),PHP 的默认 SoapServer 和 WCF 都不支持。尽管如此,要获得下载或上传进度的反馈,您需要做很多工作。

一种替代方法是使用 Base64 对数据进行编码,但即使这样也有一些缺点
速度相当慢,数据会膨胀,并且很难获得传输进度的反馈。

那么为什么不通过独立的原始 HTTP 请求将数据与 SOAP 消息一起传输呢?数据无需编码,实现进度反馈也很容易,因为它是一个标准的 HTTP 请求,与 SOAP 无关。

假设我们有以下方法

/**
 * @return string
 * @access web
 */
public function GetData()
{
  return file_get_contents("picture.jpg");
}

传输返回数据的活动图会看起来像

Activity Diagram
  1. 告诉客户端返回的值是一种可以访问实际数据的密钥
  2. 将二进制数据(带密钥)保存在某种数据库或硬盘驱动器中
  3. 在 SOAP 调用中返回一个指向数据库条目的密钥,而不是数据
  4. 提供一个(非 SOAP)脚本,客户端可以通过传递密钥来获取数据
  5. 删除数据库中的数据

客户端上传数据的流程并没有太大区别

  1. 告诉客户端参数类型是一种密钥(可以通过上传数据获得)
  2. 提供一个(SOAP)Web 服务,该服务返回这样的密钥(例如上传票证)
  3. 提供一个(非 SOAP)脚本,客户端可以通过传递密钥上传数据,并将此数据(带密钥)保存在数据库中
  4. 当客户端使用此密钥调用(SOAP)方法时,用数据库中存储的数据替换密钥
  5. 删除数据库中的数据

对于第一步,必须实现 IWsdlTypeMapping 接口,以便将数据类型映射到密钥。

因为“string”已经映射到“xsd:string”,所以数据类型必须更改为“binary”或其他内容(“@return binary”或“@param binary”)。

步骤 1-5 可以由实现 IObjectConverter 接口的对象完成。

Convert 方法可以执行下载数据的步骤 2 和 3,ConvertBack 方法执行上传数据的步骤 4。

建议包含一个完整的 URL 和密钥,以便客户端只需获取此 URL 即可下载或上传数据。

除了提到的优点之外,客户端还可以一次上传多个数据(多个方法调用需要),可能进行 zip 压缩以加快传输速度。

祝您编码愉快!

说实话,我已经写了一个这样的扩展,但它需要我的容器和缓存框架,我还没有发布。尽管如此,它可以在上面的下载部分下载。

示例

示例分为几个文件:index.php 是入口点,security.php 包含负责身份验证的 Security 类,TestWebservice.php 包含示例 Web 服务。

我认为示例已充分注释,无需进一步解释。

C# 客户端

感谢 WCF 和 Visual Studio,用 C# 编写 SOAP 客户端非常容易。

首先,创建一个新的 C# 项目,最好是控制台应用程序。
在解决方案资源管理器中,右键单击“引用”,然后单击“添加 Web 引用”。
输入 WSDL 文档的 URL(对于给定示例,http://www.hediet.de/projects/enhanced-soap-webservices/code/index.php/services.wsdl),然后单击“转到”,Visual Studio 将解析 WSDL 文档并列出所有找到的服务及其方法。为引用的命名空间指定一个有意义的名称,然后单击“添加引用”。

使用生成的客户端

要调用 Web 服务的方法,只需将您在“添加 Web 引用”对话框中提供的命名空间添加到 using 部分并创建一个客户端

TestPortType testClient = new TestPortTypeClient();
testClient.SomeMethod();
为每个服务都会生成一个特定的客户端。这些客户端的配置存储在 app.config 中。

身份验证

Http Basic 身份验证

正如我在前面的章节中提到的,WCF 支持 Http Basic 身份验证。要启用此功能,请转到解决方案资源管理器中的 app.config 文件。

编辑安全节点(configuration/system.serviceModel/bindings/basicHttpBinding/binding/security),使其看起来像

<security mode="Transport">
  <transport clientCredentialType="Basic" proxyCredentialType="None"
    realm="MyRealm" />
  <message clientCredentialType="UserName" algorithmSuite="Default" />
</security>

领域必须与响应标头中的域(参见身份验证章节)相同。

创建客户端后,必须设置凭据

client.ClientCredentials.UserName.UserName = "user";
client.ClientCredentials.UserName.Password = "password";

重要提示:WCF 需要启用 SSL(https 而不是 http)!

如果证书无效,您可以使用以下代码片段禁用验证检查

private static bool ValidateRemoteCertificate(object sender,
  X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors)
{
  return true;
}
[...]
ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate;

Http Header 身份验证

另一种方法是使用自定义标头传递凭据。

为此,既不需要编辑 app.config,也不需要使用 SSL 协议,但必须开发一个自定义客户端消息检查器来拦截 WCF 的通信。

要插入消息检查器,必须通过实现 IEndpointBehavior 来编写自定义终结点行为

public class CredentialHeaderBehavior : IEndpointBehavior
{
    private readonly NetworkCredential credential;

    public CredentialHeaderBehavior(NetworkCredential credential)
    {
        this.credential = credential; //The credential to pass with the header
    }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Dispatcher.ClientRuntime behavior)
    {
		//Add the custom header message inspector
        behavior.MessageInspectors.Add(new CredentialHeaderMessageInspector(credential));
    }

	//Not used methods, but required when implementing IEndpointBehavior
    public void AddBindingParameters(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Channels
        .BindingParameterCollection bindingParameters) { }

    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Dispatcher
        .EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint serviceEndpoint) { }
}

以及 CredentialHeaderMessageInspector,它实际上设置了标头

internal class CredentialHeaderMessageInspector : IClientMessageInspector
{
    private readonly NetworkCredential credential;

    public CredentialHeaderMessageInspector(NetworkCredential credential)
    {
        this.credential = credential;
    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request,
        System.ServiceModel.IClientChannel channel)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
        else
        {
            httpRequestMessage = new HttpRequestMessageProperty();
            request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);
        }

		//Add the username and password to the headers
        httpRequestMessage.Headers["USER"] = credential.UserName;
        httpRequestMessage.Headers["PASSWORD"] = credential.Password;

        return null;
    }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply,
        object correlationState) { }
}

扩展点

除了提到的可能扩展之外,还可以实现以下功能

  • Ajax 访问

    可以使用 JavaScript 访问 Web 服务。

  • Base64 支持

    类型“base64”可以在预处理中自动解码,在后处理中自动编码。

历史

  • 版本 1.2 - 添加了 C# 演示,改进了 PHP 演示和解释,引入了新的身份验证方法
  • 版本 1.1 - 修正了一些拼写错误
  • 版本 1.0 - 初始文章
© . All rights reserved.