Home
张文涛的个人博客
致力于提供Drupal开发,实践等方面的精品资讯

Main navigation

  • 首页
  • Glossary

Breadcrumb

  • Home
  • Drupal settings.php 进阶配置(代理 / 真实 IP / 访问拦截 / 调试)
By admin | 11:04 AM CST, Wed June 30, 2021

settings.php 是 Drupal 启动阶段最早执行的文件之一,它跑在容器、模块、数据库之前。这个位置很特殊:你能在这里改 $_SERVER、拦掉请求、给 Guzzle 塞代理,代价是没有界面、没有日志、也没有任何兜底——写错一个分号就是整站白屏,连报错页都出不来。

下面这些片段都是线上真正用过的,按用途分组,每条标注了适用版本和副作用。全部建议配合第四节的环境隔离方式使用。

一、出站请求走代理

Drupal 所有对外请求都走 Guzzle:聚合、oEmbed 探测、更新检查、第三方 API 调用。服务器在墙内或出网受限时,可以让这些请求统一走代理,不必逐个模块改代码。

$settings['http_client_config']['proxy']['http'] = 'http://user:pass@proxy.example.com:8080';
$settings['http_client_config']['proxy']['https'] = 'http://user:pass@proxy.example.com:8080';

// 这些主机直连,不走代理
$settings['http_client_config']['proxy']['no'] = ['127.0.0.1', 'localhost', 'internal.example.com'];

两点提醒。第一,代理地址别写真实公网 IP——一旦进了公开仓库或博客,等于把一个开放代理送出去;能加认证就加,URL 里直接带 user:pass@ 就行。第二,这是全局配置,会影响所有模块的出站请求,代理挂掉时的表现是"所有外部请求一起超时",排查起来容易跑偏。

顺手提一句:很多版本的这个片段后面还跟着一行 $settings['http_client_config']['verify'] = false;,不建议保留。它关掉的是所有出站 HTTPS 的证书校验,等于给中间人攻击开门。内网自签证书的正确解法是指定 CA 包:

// 只为自签证书的内网加信任链,而不是关掉校验
$settings['http_client_config']['verify'] = '/etc/ssl/certs/internal-ca.crt';

二、反向代理与真实 IP

站点前面挂了 LB、CDN 或 Docker 网关时,PHP 看到的 REMOTE_ADDR 全变成代理地址,flood control、访问日志、IP 屏蔽会一起失效。要告诉 Drupal 谁才是真正的代理:

$settings['reverse_proxy'] = TRUE;

// 只列真实的反代地址,支持 IPv4/IPv6 和 CIDR
$settings['reverse_proxy_addresses'] = [
  '10.0.0.10',
  '172.17.0.0/16',
];

不要写成 [$_SERVER['REMOTE_ADDR']]。core 的注释写得很直白:"it is likely that this would allow IP address spoofing"。那行等于无条件信任任何来访者带来的 X-Forwarded-For,攻击者随便改个头就能伪造来源 IP,本文后面所有基于 IP 的判断都会失效。只有代理地址确实无法穷举(比如用 CDN)时才考虑这条路,而且必须配合 WAF 侧清洗 header。

代理不用 X-Forwarded-For 时(比如 Cloudflare 的 CF-Connecting-IP),还要额外指定 header 和信任范围:

$settings['reverse_proxy_header'] = 'HTTP_CF_CONNECTING_IP';

$settings['reverse_proxy_trusted_headers'] =
  \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_FOR |
  \Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PROTO;

三、访问拦截

拦垃圾爬虫、扫描器和恶意 IP,常规做法是装模块或改 nginx。写在 settings.php 里的好处是它比 Drupal 的 bootstrap 更早,容器、数据库、缓存都不用初始化,成本几乎为零;坏处是绕过 Drupal 的日志和 403 页面,也不会留下任何访问记录。所以这部分的定位是拦截已知噪音,不是做访问控制。

3.1 按 URI 拦截

$bad_uri = [
  '?title1=&title=',
  '?title=&title1=',
  'accounting?page=',
];

$request_uri = $_SERVER['REQUEST_URI'] ?? '';
foreach ($bad_uri as $uri) {
  if (str_contains($request_uri, $uri)) {
    http_response_code(403);
    print 'bad visit';
    exit;
  }
}

3.2 按 User-Agent 拦截

$bad_bots = [
  'YisouSpider',
  'MQQBrowser',
  'Mb2345Browser',
  'LieBaoFast',
  'UCBrowser',
];

$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
foreach ($bad_bots as $bot) {
  if ($user_agent !== '' && str_contains($user_agent, $bot)) {
    http_response_code(403);
    print htmlspecialchars($bot) . ' Not allowed.';
    exit;
  }
}

UA 是客户端随便改的,这么写只挡得住"懒得伪装"的爬虫,别指望它防有心人。

3.3 按 IP / CIDR 拦截

$ban_ip = [
  '194.87.68.143',
  '10.10.255.0/24',
];

$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
foreach ($ban_ip as $cidr) {
  if (\Symfony\Component\HttpFoundation\IpUtils::checkIp($client_ip, $cidr)) {
    http_response_code(403);
    print 'Access denied.';
    exit;
  }
}

IpUtils::checkIp() 就是 core 的 ban 模块在用的那个 helper,支持 IPv4/IPv6 和 CIDR,不用自己写位运算。这里刻意取 REMOTE_ADDR:settings.php 执行得太早,\Drupal::request() 和容器都还拿不到,所以只能读原始变量。站点如果在反代后面,正确的顺序是先在第二节把 reverse_proxy_addresses 配对,再考虑把这套判断挪到中间件里用 getClientIp()。

3.4 双重 X-Forwarded-For 比对

有些代理会在 X-Forwarded-For 之外再塞一个 X-Original-Forwarded-For。两个值完全一致,通常意味着请求被重复转发过,或者 header 被人为构造过,可以直接丢弃:

if (!empty($_SERVER['HTTP_X_ORIGINAL_FORWARDED_FOR'])
  && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {

  $original = strtolower(trim($_SERVER['HTTP_X_ORIGINAL_FORWARDED_FOR']));
  $current  = strtolower(trim($_SERVER['HTTP_X_FORWARDED_FOR']));

  if ($original === $current) {
    http_response_code(403);
    exit;
  }
}

这条规则强依赖代理厂商的 header 清洗行为,换环境前先在测试机上看一眼真实的 header 组合,别直接照搬。

四、调试配置

开发机上这套组合很好用:

// devel 模块提供
$config['devel.settings']['devel_dumper'] = 'var_dumper';
$config['system.logging']['error_level'] = 'verbose';

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);

但绝不要留在生产环境。error_level = verbose 会把完整错误信息——文件路径、SQL 片段、函数参数——直接输出给匿名访客。正确做法是分环境:调试片段挪进 settings.local.php,这个文件不进仓库,core 的 settings.php 末尾默认就留了 include 口子。

// settings.php 末尾
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
  include $app_root . '/' . $site_path . '/settings.local.php';
}

这样 settings.php 只放和环境无关的通用配置,本地、预发、生产各自维护自己的 local 文件。本文所有带副作用的片段——代理、错误显示、临时拦截规则——都建议按这个方式隔离,最省事的做法是把生产不想要的东西全都推给 local。

五、强制 HTTPS

Drupal 7 时代的老办法,直接改服务器变量:

// 仅 Drupal 7
$_SERVER['HTTPS'] = 'on';
$_SERVER['SERVER_PORT'] = 443;

Drupal 8 及以上不要这么写。跳转应该交给 nginx 或 LB;如果 TLS 在代理上终结、站点又需要知道自己是 HTTPS,就用第二节的 reverse_proxy 加上 HEADER_X_FORWARDED_PROTO,让 Drupal 自己解析协议,而不是硬改超全局变量。

六、这些配置该不该放 settings.php

需求 推荐层次 说明
UA / IP 黑名单、堵扫描器 nginx map、Cloudflare WAF 连接层就拒掉,不占 PHP 进程,而且有完整访问日志
要按用户、权限、内容做判断 自定义中间件或 EventSubscriber 能拿到 Request 对象,能写 watchdog,可测试可维护
只想快速止血、动不了 web 服务器 settings.php 本文这些片段,胜在零依赖、改完立刻生效
出站代理、真实 IP、错误级别 settings.php 本来就属于环境配置,没有更合适的地方

小结

  • settings.php 跑在 bootstrap 之前,适合做"环境层"的事:出站代理、真实 IP、错误级别;
  • 拿它做访问拦截是权宜之计,省资源但不留痕迹,规模上来就该上 WAF 或中间件;
  • reverse_proxy_addresses 配错等于把所有 IP 判断变成摆设,这是全文最容易踩的坑;
  • 带副作用的配置一律隔离到 settings.local.php,别让调试代码跟着上生产。

Glossary

  • October 2026 (4)
  • September 2026 (3)
  • August 2026 (4)
  • July 2026 (2)
  • June 2026 (2)
  • March 2023 (1)
  • October 2022 (1)
  • October 2021 (2)
  • August 2021 (3)
  • June 2021 (2)
  • May 2021 (1)
  • April 2021 (2)
  • November 2020 (1)
  • June 2020 (2)
  • December 2019 (2)
  • November 2019 (1)
  • August 2019 (1)
  • February 2018 (1)
  • October 2017 (1)
  • September 2017 (1)
  • July 2017 (1)
  • June 2017 (2)
  • May 2017 (1)
  • March 2017 (3)
  • December 2016 (2)
  • November 2016 (4)
  • October 2016 (3)
  • June 2016 (1)
  • December 2015 (1)
  • November 2015 (6)
  • October 2015 (2)
  • September 2015 (1)
  • August 2015 (1)
  • July 2015 (1)
  • June 2015 (1)
  • May 2015 (1)
  • March 2015 (1)
  • January 2015 (1)
  • December 2014 (3)
  • October 2014 (4)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (1)
  • May 2014 (1)
  • April 2014 (2)
  • March 2014 (2)
  • February 2014 (2)
  • January 2014 (4)
  • December 2013 (1)
  • March 2013 (2)
  • February 2013 (2)
  • December 2012 (2)
  • November 2012 (4)
  • October 2012 (1)
  • September 2012 (6)
  • August 2012 (15)
  • July 2012 (5)
  • June 2012 (13)
  • May 2012 (9)
  • April 2012 (8)
  • March 2012 (9)
  • February 2012 (3)
  • January 2012 (1)
  • December 2011 (1)
  • November 2011 (6)
  • October 2011 (2)
  • September 2011 (17)
  • August 2011 (4)
  • July 2011 (4)
  • June 2011 (5)
  • May 2011 (4)

Copyright © 2026

苏ICP备14044171号-1