Drupal 8:在 Shield 模块中戳一个洞

Shield 模块通过在整个网站上放置 Apache 身份验证系统来阻止对 Drupal 站点的访问。这意味着要访问该站点,您需要用户名和密码。

这在许多不同的情况下都很有用,但我最常使用它来保护开发和临时站点不被访问。它不是世界上最复杂的身份验证系统,但足以防止因临时站点被搜索引擎抓取而造成的尴尬。

它确实引入的一个问题是在开发 API 端点时。因为 Shield 模块会阻止访问整个站点,这意味着测试 API 端点的唯一方法是关闭该模块,从而暴露整个站点。即使我们让 Shield 处于开启状态,也存在一个问题,即 API 端点通常有自己的身份验证系统,他们经常会在屏蔽身份验证和他们自己的身份验证之间混淆,这可能会导致一些麻烦。

值得庆幸的是,Drupal 8 允许我们覆盖此功能并在 Shield 模块中戳洞,以允许某些端点在未经身份验证的情况下公开。我们需要做的是覆盖 Shield 模块的服务定义并引入我们自己的类。我们的自定义类将检查传入的请求并根据某些条件进行检查。如果满足条件,则我们允许请求正常发生。如果他们没有通过,那么我们将请求发送到 Shield 模块,请求将通过身份验证系统。

我应该指出,禁用屏蔽身份验证会使路由开放访问。因此,您应该为匿名访问这些端点做好准备,或者准备好另一个身份验证系统。

在站点上加载服务时,Drupal 将自动加载可用于更改服务的服务提供者类。当缓存被清除时,这些类可用于以某种方式更改服务。这些类只有在满足某些要求时才会被加载。这些要求如下:

  • 模块的机器名称被翻译成删除所有空格并将每个单词的第一个字母大写。

  • 类名保存在模块的 /src 目录中。

  • 类名必须以 ServiceProvider 为后缀。

  • 该类必须扩展 ServiceProviderBase。

为方便起见,我们需要创建一个模块,在本例中,我们将其命名为“Shield Hole”,并为其指定机器名称“shield_hole”。因此我们的类需要被称为 ShieldHoleServiceProvider。

<?php
 
namespace Drupal\shield_hole;
 
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
 
/**
 * Class ShieldHoleServiceProvider.
 *
 * @package Drupal\shield_hole
 */
class ShieldHoleServiceProvider extends ServiceProviderBase {
 
  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    // 装饰屏蔽模块以防止它在某些情况下触发
    // 路线。
    $definition = $container->getDefinition('shield.middleware');
    $definition->setClass('Drupal\shield_hole\ShieldOverride');
  }
 
}

当我们清除缓存时,Drupal 加载此类并运行该alter()方法,将所有定义的服务作为 $container 参数传入。在这种情况下,我们找到shield.midderware类的定义并将其替换为我们自己的 ShieldOverride 类。

ShieldOverride 类非常简单,但它只有一项工作要做。通过扩展 ShieldMiddleware 类,我们可以使用它或完全绕过它。该handle()方法在页面请求进入时被调用,因此它可以访问该页面请求的所有元数据。然后我们检测需要禁用身份验证的正确页面。如果是这种情况,那么我们会使用正常的 Drupal 响应来处理请求(通过 HTTP 内核对象完成)。

<?php
 
namespace Drupal\shield_hole;
 
use Drupal\shield\ShieldMiddleware;
use Symfony\Component\HttpFoundation\Request;
 
/**
 * Class ShieldOverride.
 *
 * @package Drupal\shield_hole
 */
class ShieldOverride extends ShieldMiddleware {
 
  /**
   * {@inheritdoc}
   */
  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
    // 获取当前请求 URI。
    $currentPath = $request->getRequestUri();
 
    // 获取当前方法(例如 GET 或 POST)。
    $currentMethod = $request->getMethod();
 
    if (($currentMethod == 'POST' || $currentMethod == 'GET') && strstr($currentPath, '/services/some_api') !== FALSE) {
      // 如果我们尝试访问该服务,那么我们将处理 
      // 请求而不调用 Shield 模块。
      return $this->httpKernel->handle($request, $type, $catch);
    }
 
    // 始终使用默认的 Shield 行为处理请求。
    return parent::handle($request, $type, $catch);
  }
 
}

默认行为是使用父对象handle()方法处理请求。由于我们扩展了 ShieldMiddleware 类,父handle()方法本质上是身份验证系统。这意味着如果请求不符合我们的要求,那么它会被屏蔽模块处理,并要求用户输入密码。

这个使用 ServiceProvider 类更改现有服务的示例在 Drupal 8 中有很多用途。 Shield 模块覆盖是一个很好的示例,因为它展示了如何更改简单服务以提供已知结果。您只需要了解这样做的安全隐患。

当我需要改变模块运行的方式时,我已经多次使用 ServiceProvider 类来重载贡献模块中的现有类。这在执行测试时特别有用。如果您正在尝试运行一些测试,尤其是针对 API,那么使用 ServiceProvider 来模拟服务是一种很好的方式,可以在不实际使用 API 的情况下为您的应用程序提供已知数据集。

有关此主题的更多信息,您可以查看 Drupal 8 服务提供商文档。