进行框架间的系统调用

开发 PSR-7(和中间件)的主要原因之一是对框架之间调用的需求越来越大。值得注意的是,PSR-7 的主要文档由 PHP Framework Interop Group (PHP-FIG) 主持。

如何做…

1.在中间件框架间调用中使用的主要机制是创建一个驱动程序,连续执行框架调用,维护一个共同的请求和响应对象。请求和响应对象有望分别代表 Psr\Http\Message\ServerRequestInterfacePsr\Http\Message\ResponseInterface

  1. 为了这个说明的目的,我们定义了一个中间件会话验证器。常量和属性反映了会话指纹,这是我们用来整合网站访问者的 IP 地址、浏览器和语言设置等因素的术语。
  1. namespace Application\MiddleWare\Session;
  2. use InvalidArgumentException;
  3. use Psr\Http\Message\ {
  4. ServerRequestInterface, ResponseInterface };
  5. use Application\MiddleWare\ { Constants, Response, TextStream };
  6. class Validator
  7. {
  8. const KEY_TEXT = 'text';
  9. const KEY_SESSION = 'thumbprint';
  10. const KEY_STATUS_CODE = 'code';
  11. const KEY_STATUS_REASON = 'reason';
  12. const KEY_STOP_TIME = 'stop_time';
  13. const ERROR_TIME = 'ERROR: session has exceeded stop time';
  14. const ERROR_SESSION = 'ERROR: thumbprint does not match';
  15. const SUCCESS_SESSION = 'SUCCESS: session validates OK';
  16. protected $sessionKey;
  17. protected $currentPrint;
  18. protected $storedPrint;
  19. protected $currentTime;
  20. protected $storedTime;
  1. 构造函数把ServerRequestInterface实例和session作为参数。如果session是一个数组(比如$_SESSION),我们就把它封装在一个类中。我们这样做的原因是为了防止传递给我们一个会话对象,比如Joomla中使用的JSession。然后,我们使用前面提到的因素创建拇指指纹。如果存储的拇指印不可用,我们就假设这是第一次,如果设置了这个参数,就存储当前打印以及停止时间。我们使用了md5(),因为它是一种快速的哈希值,不对外暴露,因此对这个应用很有用。
  1. public function __construct(
  2. ServerRequestInterface $request, $stopTime = NULL)
  3. {
  4. $this->currentTime = time();
  5. $this->storedTime = $_SESSION[self::KEY_STOP_TIME] ?? 0;
  6. $this->currentPrint =
  7. md5($request->getServerParams()['REMOTE_ADDR']
  8. . $request->getServerParams()['HTTP_USER_AGENT']
  9. . $request->getServerParams()['HTTP_ACCEPT_LANGUAGE']);
  10. $this->storedPrint = $_SESSION[self::KEY_SESSION]
  11. ?? NULL;
  12. if (empty($this->storedPrint)) {
  13. $this->storedPrint = $this->currentPrint;
  14. $_SESSION[self::KEY_SESSION] = $this->storedPrint;
  15. if ($stopTime) {
  16. $this->storedTime = $stopTime;
  17. $_SESSION[self::KEY_STOP_TIME] = $stopTime;
  18. }
  19. }
  20. }
  1. 虽然不需要定义__invoke(),但这个神奇的方法对于独立的中间件类来说相当方便。按照惯例,我们接受 ServerRequestInterfaceResponseInterface 实例作为参数。在这个方法中,我们只需检查当前的拇指印是否与存储的拇指印一致。第一次,当然,它们会匹配。但在随后的请求中,意图劫持会话的攻击者有可能会被发现。此外,如果会话时间超过了停止时间(如果设置了),同样,也会发送401代码。
  1. public function __invoke(
  2. ServerRequestInterface $request, Response $response)
  3. {
  4. $code = 401; // unauthorized
  5. if ($this->currentPrint != $this->storedPrint) {
  6. $text[self::KEY_TEXT] = self::ERROR_SESSION;
  7. $text[self::KEY_STATUS_REASON] =
  8. Constants::STATUS_CODES[401];
  9. } elseif ($this->storedTime) {
  10. if ($this->currentTime > $this->storedTime) {
  11. $text[self::KEY_TEXT] = self::ERROR_TIME;
  12. $text[self::KEY_STATUS_REASON] =
  13. Constants::STATUS_CODES[401];
  14. } else {
  15. $code = 200; // success
  16. }
  17. }
  18. if ($code == 200) {
  19. $text[self::KEY_TEXT] = self::SUCCESS_SESSION;
  20. $text[self::KEY_STATUS_REASON] =
  21. Constants::STATUS_CODES[200];
  22. }
  23. $text[self::KEY_STATUS_CODE] = $code;
  24. $body = new TextStream(json_encode($text));
  25. return $response->withStatus($code)->withBody($body);
  26. }
  1. 现在我们可以把我们新的中间件类用起来了。这里总结了框架间调用的主要问题,至少在这一点上是这样。相应地,我们如何实现中间件,很大程度上取决于最后一点。
  • 并非所有的PHP框架都符合PSR-7标准
  • 现有的PSR-7实施工作不完整
  • 所有框架都想当 “老大”
  1. 举个例子,看看 Zend Expressive 的配置文件,它是一个自称 PSR7 的中间件微框架。这里有一个文件, middleware-pipeline.global.php,它位于标准Expressive应用程序的config/autoload文件夹中。依赖关系键用于识别将在管道中激活的中间件包装类。
  1. <?php
  2. use Zend\Expressive\Container\ApplicationFactory;
  3. use Zend\Expressive\Helper;
  4. return [
  5. 'dependencies' => [
  6. 'factories' => [
  7. Helper\ServerUrlMiddleware::class =>
  8. Helper\ServerUrlMiddlewareFactory::class,
  9. Helper\UrlHelperMiddleware::class =>
  10. Helper\UrlHelperMiddlewareFactory::class,
  11. // insert your own class here
  12. ],
  13. ],
  1. middleware_pipline 键下,您可以确定将在路由过程发生之前或之后执行的类。可选参数包括路径、错误和优先级。
  1. 'middleware_pipeline' => [
  2. 'always' => [
  3. 'middleware' => [
  4. Helper\ServerUrlMiddleware::class,
  5. ],
  6. 'priority' => 10000,
  7. ],
  8. 'routing' => [
  9. 'middleware' => [
  10. ApplicationFactory::ROUTING_MIDDLEWARE,
  11. Helper\UrlHelperMiddleware::class,
  12. // insert reference to middleware here
  13. ApplicationFactory::DISPATCH_MIDDLEWARE,
  14. ],
  15. 'priority' => 1,
  16. ],
  17. 'error' => [
  18. 'middleware' => [
  19. // Add error middleware here.
  20. ],
  21. 'error' => true,
  22. 'priority' => -10000,
  23. ],
  24. ],
  25. ];
  1. 另一种技术是修改现有框架模块的源代码,并向符合PSR-7的中间件应用程序提出请求。下面是一个修改Joomla!安装的例子,以包含一个中间件会话验证器。

  2. 接下来,在 /path/to/joomla 文件夹的 index.php 文件末尾添加这段代码。由于Joomla!使用Composer,我们可以利用Composer的自动加载器。

  1. session_start(); // to support use of $_SESSION
  2. $loader = include __DIR__ . '/libraries/vendor/autoload.php';
  3. $loader->add('Application', __DIR__ . '/libraries/vendor');
  4. $loader->add('Psr', __DIR__ . '/libraries/vendor');
  1. 然后我们可以创建一个中间件会话验证器的实例,并在 $app = JFactory::getApplication('site') 之前发出验证请求。
  1. $session = JFactory::getSession();
  2. $request =
  3. (new Application\MiddleWare\ServerRequest())->initialize();
  4. $response = new Application\MiddleWare\Response();
  5. $validator = new Application\Security\Session\Validator(
  6. $request, $session);
  7. $response = $validator($request, $response);
  8. if ($response->getStatusCode() != 200) {
  9. // take some action
  10. }

如何运行…

首先,创建步骤2-5中描述的 Application\MiddleWare\Session\Validator 测试中间件类。然后,你需要去 https://getcomposer.org/,并按照指示获得Composer。将其下载到/path/to/source/for/this/chapter文件夹中。接下来,构建一个基本的Zend Expressive应用程序,如下图所示。当提示需要最小框架时,一定要选择No。

  1. cd /path/to/source/for/this/chapter
  2. php composer.phar create-project zendframework/zend-expressive-skeleton expressive

这将创建一个文件夹/path/to/source/for/this/chapter/expressive。改成这个目录。修改public/index.php如下。

  1. <?php
  2. if (php_sapi_name() === 'cli-server'
  3. && is_file(__DIR__ . parse_url(
  4. $_SERVER['REQUEST_URI'], PHP_URL_PATH))
  5. ) {
  6. return false;
  7. }
  8. chdir(dirname(__DIR__));
  9. session_start();
  10. $_SESSION['time'] = time();
  11. $appDir = realpath(__DIR__ . '/../../..');
  12. $loader = require 'vendor/autoload.php';
  13. $loader->add('Application', $appDir);
  14. $container = require 'config/container.php';
  15. $app = $container->get(\Zend\Expressive\Application::class);
  16. $app->run();

然后,需要创建一个包装器类来调用我们的会话验证器中间件。创建一个SessionValidateAction.php文件,需要放在/path/to/source/for/this/chapter/expressive/src/App/Action文件夹中。在本例中,将停止时间参数设置为一个较短的持续时间。在本例中,time() + 10得到10秒。

  1. namespace App\Action;
  2. use Application\MiddleWare\Session\Validator;
  3. use Zend\Diactoros\ { Request, Response };
  4. use Psr\Http\Message\ResponseInterface;
  5. use Psr\Http\Message\ServerRequestInterface;
  6. class SessionValidateAction
  7. {
  8. public function __invoke(ServerRequestInterface $request,
  9. ResponseInterface $response, callable $next = null)
  10. {
  11. $inbound = new Response();
  12. $validator = new Validator($request, time()+10);
  13. $inbound = $validator($request, $response);
  14. if ($inbound->getStatusCode() != 200) {
  15. session_destroy();
  16. setcookie('PHPSESSID', 0, time()-300);
  17. $params = json_decode(
  18. $inbound->getBody()->getContents(), TRUE);
  19. echo '<h1>',$params[Validator::KEY_TEXT],'</h1>';
  20. echo '<pre>',var_dump($inbound),'</pre>';
  21. exit;
  22. }
  23. return $next($request,$response);
  24. }
  25. }

现在需要将新类添加到中间件管道中。修改config/autoload/middleware-pipeline.global.php如下。修改的内容以粗体显示。

  1. <?php
  2. use Zend\Expressive\Container\ApplicationFactory;
  3. use Zend\Expressive\Helper;
  4. return [
  5. 'dependencies' => [
  6. 'invokables' => [
  7. App\Action\SessionValidateAction::class =>
  8. App\Action\SessionValidateAction::class,
  9. ],
  10. 'factories' => [
  11. Helper\ServerUrlMiddleware::class =>
  12. Helper\ServerUrlMiddlewareFactory::class,
  13. Helper\UrlHelperMiddleware::class =>
  14. Helper\UrlHelperMiddlewareFactory::class,
  15. ],
  16. ],
  17. 'middleware_pipeline' => [
  18. 'always' => [
  19. 'middleware' => [
  20. Helper\ServerUrlMiddleware::class,
  21. ],
  22. 'priority' => 10000,
  23. ],
  24. 'routing' => [
  25. 'middleware' => [
  26. ApplicationFactory::ROUTING_MIDDLEWARE,
  27. Helper\UrlHelperMiddleware::class,
  28. App\Action\SessionValidateAction::class,
  29. ApplicationFactory::DISPATCH_MIDDLEWARE,
  30. ],
  31. 'priority' => 1,
  32. ],
  33. 'error' => [
  34. 'middleware' => [
  35. // Add error middleware here.
  36. ],
  37. 'error' => true,
  38. 'priority' => -10000,
  39. ],
  40. ],
  41. ];

也可以考虑修改主页模板来显示$_SESSION的状态。这个文件是/path/to/source/for/this/chapter/expressive/templates/app/home-page.phtml。只需添加var_dump($_SESSION)就可以了。

最初,应该看到这样的东西。

进行框架间的系统调用 - 图1

10秒后,刷新浏览器。现在你应该看到这个。

进行框架间的系统调用 - 图2