返回

Drupal 7 /user跑偏? 强制匿名用户访问登录页

php

好的,这是你要的博客文章:

Drupal 7: /user 跑偏了?强制匿名用户访问 /user/login

问题来了

搞 Drupal 7 开发的时候,你可能遇到过这么个情况:你兴冲冲地用 user-login.tpl.php 文件定制了一个超炫的登录页面,想着用户一进来就能看到你的杰作。结果呢?一测试发现,好多本该去 /user/login 的匿名用户,直接被引到了 /user 路径。

麻烦的是,你并不想动 user.tpl.php 文件,因为那玩意儿是管用户个人主页(Profile Page)样式的,改了那边,登录用户的个人主页就跟着变样了,这可不是我们想要的。

你可能像提问者一样,在网上搜了一圈,找到了些在主题 template.php 文件里加代码的法子,比如用 hook_preprocess_page 来检查用户是否登录,如果没登录且访问的是 /user/user/login,就尝试加载一个特定的模板文件(像 page-login.tpl.php)或者直接跳转。

像这样的代码:

<?php
function mytheme_preprocess_page(&$vars) {
  // 检查用户是否登录,以及是否在目标页面
  if ($vars['user']->uid == 0 && arg(0) == 'user' && (arg(1) == '' || arg(1) == 'login')) {
    // 添加自定义模板文件
    array_unshift($vars['template_files'], 'page-login');
    // 添加 body class 便于 CSS 控制
    $vars['body_classes'] .= ' logged'; // 注意这里可能出错
  }
}
?>

结果呢?要么报错,说什么 array_unshift() 的参数不对,body_classes 没定义:

Warning: array_unshift() expects parameter 1 to be array, null given in framework_preprocess_page() ...
Notice: Undefined index: body_classes in framework_preprocess_page() ...

要么就是去掉添加 class 的那行,虽然不报 Notice 了,但 array_unshift() 的 Warning 还在,关键是——它根本没起作用!页面还是老样子。

然后你可能又试了直接用 drupal_goto() 跳转:

<?php
function YOURTHEME_preprocess_page(&$vars) {
  // 检查用户是否登录,以及是否在目标页面
  if ($vars['user']->uid == 0 && arg(0) == 'user' && (arg(1) == '' || arg(1) == 'login')) {
    drupal_goto("user/login");
  }
}
?>

这下更糟,页面直接卡死,浏览器提示“重定向次数过多”或者“无法完成请求,服务器重定向方式有问题”。但奇怪的是,如果把 drupal_goto() 的目标改成别的路径,比如 drupal_goto("some-other-page");,跳转却又是好的。偏偏就是跳转到 user/login 不行。

到底是怎么回事?怎样才能让访问 /user 的匿名用户乖乖地去 /user/login 呢?

为啥会这样?(原因分析)

咱们来捋一捋为啥上面那些尝试会失败。

  1. preprocess_page + array_unshift 的问题

    • 时机太晚,且功能不对hook_preprocess_page 这个钩子,主要是在页面数据准备好、快要渲染模板之前,给你一个机会可以修改传给页面模板(page.tpl.php)的变量。用 array_unshift$vars['template_files'] 里加模板建议,目的是让 Drupal 尝试使用 不同的 .tpl.php 文件来渲染 当前路径/user),而不是把你 送到 另一个路径(/user/login)。这根本就不是重定向!
    • 变量未初始化 :至于那些 Warning 和 Notice,是因为在 preprocess_page 执行的这个阶段,$vars['template_files'] 或者 $vars['body_classes'] 不一定总是个数组。特别是如果页面处理过程中出了点小差错,或者某些模块没有正确初始化这些变量,你直接往上操作就可能出错。
  2. preprocess_page + drupal_goto('user/login') 的问题

    • 坑爹的重定向循环 :这个是问题的关键!hook_preprocess_page 依然是在页面处理的较后阶段执行。当你试图在处理 /user 路径的请求时,触发 drupal_goto('user/login'),服务器会发出一个 302 重定向响应,让浏览器去访问 /user/login
    • 浏览器收到响应,乖乖地请求 /user/login
    • 服务器开始处理 /user/login 请求。重点来了:对于 /user/login 这个路径,你的 preprocess_page 钩子里的条件 arg(0) == 'user' && arg(1) == 'login' 同样成立
    • 于是,drupal_goto('user/login') 又被执行了…… 服务器再次告诉浏览器:“嘿,去 /user/login!”。
    • 浏览器:“???又来?好吧,再请求 /user/login……”。
    • 如此反复,直到浏览器或者服务器扛不住了,报出“重定向次数过多”的错误。这就是典型的重定向循环。
    • 为啥跳转到其他页面就没问题?因为当你跳转到比如 some-other-page 时,下一个请求处理的是 some-other-page,这时 arg(0) 不再是 user,你的 if 条件不满足,drupal_goto() 不会再次执行,循环自然就断了。

所以,核心问题在于:执行重定向的逻辑放错了地方(太晚),并且判断条件不够精确,导致了死循环。

我们需要在 Drupal 处理请求的更早期阶段介入,并且确保重定向逻辑只在访问 /user (且非 /user/login) 时对匿名用户触发一次。

来,咱们解决它!(解决方案)

别慌,有几种方法可以搞定这个问题。推荐使用自定义模块,因为它更健壮、更符合 Drupal 的做事方式。

方案一:创建自定义模块 + hook_boot() (推荐)

这是最推荐的方式。为啥用 hook_boot()?因为它在 Drupal 的引导(bootstrap)过程中执行得非常早,甚至在大部分模块加载和会话处理之前,也比 template.php 里的钩子早得多。在这个阶段进行重定向,可以避免后面复杂的处理流程,也不会陷入主题层面的逻辑。

原理和作用:

hook_boot() 在每次页面请求时都会执行(包括匿名用户的缓存页面请求),非常适合做一些全局的、底层的检查和操作。我们就在这里检查当前请求路径和用户登录状态,如果满足条件(匿名用户访问 /user),就直接发出重定向指令。

操作步骤:

  1. 创建一个简单的自定义模块:

    • 在你 Drupal 站点的 sites/all/modules/custom (如果 custom 目录不存在就创建一个)目录下,新建一个文件夹,比如叫 my_redirect
    • my_redirect 文件夹里创建两个文件:my_redirect.infomy_redirect.module
  2. 编写 my_redirect.info 文件:
    这个文件告诉 Drupal 你的模块是啥。

    name = My User Redirect
    description = Redirects anonymous users from /user to /user/login.
    core = 7.x
    package = Custom
    
  3. 编写 my_redirect.module 文件:
    这是模块的核心代码。

    <?php
    
    /**
     * @file
     * Contains hook implementations for the My Redirect module.
     */
    
    /**
     * Implements hook_boot().
     *
     * Runs on every page load very early in the bootstrap process.
     */
    function my_redirect_boot() {
      // 检查用户是否是匿名用户 (uid 为 0)
      // global $user; // 在 hook_boot() 中 $user 可能尚未完全加载,使用 user_is_logged_in() 更安全
      $is_anonymous = !user_is_logged_in();
    
      // 获取当前请求的内部路径 (比如 'user''user/login')
      // 使用 request_path() 比直接读 $_GET['q'] 更推荐
      $current_path = request_path();
    
      // 如果是匿名用户,并且访问的是 'user' 路径(而不是 'user/xxx''user/login' 等子路径)
      if ($is_anonymous && $current_path == 'user') {
        // 执行重定向到 user/login
        // 注意:在 hook_boot() 中使用 drupal_goto() 会中断后续的引导过程,
        // 直接进行 header 跳转是更常见的做法。
        // 但 drupal_goto() 在这里也能工作,并且处理了 exit。
        drupal_goto('user/login');
    
        // 如果你想更底层一点,可以用 header(),但要确保自己处理后续逻辑。
        // header('Location: ' . url('user/login', array('absolute' => TRUE)), TRUE, 302);
        // drupal_exit(); // 必须调用 drupal_exit() 来停止脚本执行
      }
    }
    
  4. 启用模块:

    • 访问你的 Drupal 站点的 "模块" 管理页面(通常是 /admin/modules)。
    • 找到你刚创建的 "My User Redirect" 模块(在 "Custom" 包下)。
    • 勾选它,然后保存配置。
    • 重要 :启用模块后,务必 清除 Drupal 的所有缓存 (访问 /admin/config/development/performance 点击 "清除所有缓存")。

现在,再试试看!当匿名用户访问 /user 时,应该会被干净利落地重定向到 /user/login,而不会再看到 /user 页面或遇到重定向循环。访问 /user/login 本身或者其他页面则不受影响。

进阶使用技巧:

  • 性能考量hook_boot() 在每次请求时都跑,虽然上面的代码逻辑很简单,几乎没啥性能损耗。但如果你在这个钩子里加了很复杂的逻辑,就要考虑性能影响了。不过对于这个特定的重定向需求,hook_boot() 非常合适。
  • 模块权重 :一般不需要调整。但如果你有其他模块也在 hook_boot() 里做了跟路径或用户状态相关的操作,并且需要保证你的重定向优先执行,你可能需要在数据库的 system 表里调整 my_redirect 模块的 weight 值,让它更小(比如 -10),从而更早执行。不过,对于这个场景,默认权重通常就够了。

安全建议:

这个重定向逻辑本身没什么特别的安全风险。确保你的 user/login 页面是安全的,并且遵循 Drupal 的安全最佳实践即可。

方案二:使用 Rules 模块 (无代码方式)

如果你不想写代码,或者你的站点已经用了 Rules 模块,也可以用它来配置这个重定向。

原理和作用:

Rules 模块提供了一个强大的 UI,让你能定义 "事件-条件-动作" 规则。我们可以创建一个规则,当某个事件发生(比如 Drupal 初始化)并且满足某些条件(匿名用户、访问特定路径)时,执行一个动作(页面重定向)。

操作步骤:

  1. 确保 Rules 模块已安装并启用: 如果没有,你需要先下载安装启用它及其依赖项。
  2. 创建新规则:
    • 访问 Rules 管理界面(通常是 /admin/config/workflow/rules)。
    • 点击 "添加新规则"。
    • 给规则起个名字,比如 "Redirect anonymous from /user to /user/login"。
  3. 配置事件 (Event):
    • 选择一个较早的系统事件。比较合适的可能是 "Drupal is initializing" (init) 或者 "Content is viewed"(如果用这个,要注意执行时机可能比 hook_boot 晚一点)。"Drupal is initializing" 理论上更早,更接近 hook_boot 的效果。
  4. 配置条件 (Condition):
    • 添加第一个条件:选择 "User" 分类下的 "User is anonymous"。
    • 添加第二个条件:选择 "Data comparison" 或 "Text comparison"(取决于 Rules 版本和可用条件)。
      • 目标是比较当前页面的路径。你需要找到一个代表当前路径的 data selector,比如 site:current-page:path
      • 比较类型选择 "equals"。
      • 比较值填写 user
    • 确保两个条件是 "AND" 关系。
  5. 配置动作 (Action):
    • 添加动作:选择 "System" 分类下的 "Page redirect"。
    • 在 "URL" 字段,输入 user/login
  6. 保存规则。
  7. 清除缓存: 同样,别忘了清除 Drupal 的缓存。

这种方法的好处是完全通过 UI 操作,不需要写 PHP 代码。缺点是需要额外安装和维护 Rules 模块,对于这么简单的功能来说,可能有点“杀鸡用牛刀”,并且 Rules 本身也有一定的性能开销。

安全建议:

使用 Rules 模块本身是安全的,但配置错误可能导致意外行为。仔细检查你的条件和动作设置。

为啥当初 template.php 的尝试不行?(小结一下)

回头看,template.php 里的 preprocess_page 主要负责 主题层 的数据准备。它发生得太晚,不适合做 请求路由层 的重定向决策,特别是当重定向目标本身也会触发相同逻辑时。尝试在那里用 array_unshift 修改模板建议,更是“驴唇不对马嘴”,完全跑偏了方向。

选择正确的 Drupal API 和钩子,在合适的时机介入,是解决这类问题的关键。对于需要在早期、低层次处理请求逻辑(如全局重定向)的场景,hook_boot() 或类似的早期钩子(通过自定义模块实现)通常是更靠谱的选择。