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,别让调试代码跟着上生产。