前端文件提交的安全防护
本文最后更新于168 天前,其中的信息可能已经过时,如有错误请发送邮件到184874483@qq.com

权限管理。理解并配置好它,是保证网站安全、有序运行的基石。我们可以将其拆解为“理解默认角色”和“进行权限配置”两部分。为了让整个权限体系一目了然,我们先通过一张图来概览WordPress核心用户角色的“权力阶梯”及其在投稿系统中的关键权限:

上图展示了从低到高的角色权限。对于构建的“前端投稿-后台审核”系统,投稿者 (Contributor) 和订阅者 (Subscriber) 是最相关的两个角色。

权限隔离

暴露了服务器文件路径是一个严重的安全隐患。这通常意味着上传的文件被直接链接到了服务器的绝对路径(例如 http://你的网站.com/wp-content/uploads/...),这会让技术型用户窥探到你网站的部分目录结构。

问题的根源在于 “对上传文件的访问控制不够严格” 。我们需要建立两层防护:

  1. 第一层:隐藏路径 – 确保前端不直接显示服务器物理路径。
  2. 第二层:访问拦截 – 即使有人猜到了文件路径,也无法直接通过浏览器访问,必须经过网站程序(和权限检查)。

 加固步骤一:配置Flamingo插件(隐藏路径)

我们的目标是让Flamingo后台只显示用于管理的文件链接,而不暴露具体路径。

  1. 进入Flamingo设置:在WordPress后台,进入 Flamingo → 设置
  2. 检查“文件上传”设置:找到与“文件上传”或“附件链接”相关的选项。
  3. 修改链接类型(关键):如果存在相关选项,尝试将文件链接的显示方式从 “绝对URL” 或 “文件路径” 改为 “相对URL” 或仅显示文件名。不同版本的插件设置可能不同,如果找不到,这一步可以暂时跳过,我们通过第二步从根本上解决。

 加固步骤二:保护上传目录(根本性解决)

这是最有效的一步,通过服务器规则禁止直接访问存放投稿文件的目录,只允许WordPress程序本身(经过权限验证后)读取文件。

  1. 找到上传目录路径:使用FTP工具或主机商的文件管理器,进入你网站的 wp-content/uploads/ 目录。
  2. 创建保护文件:在该目录下,检查并创建一个名为 .htaccess 的文件(如果已存在,则编辑它)。
  3. 添加安全规则:在 .htaccess 文件中,加入以下核心代码:
# 阻止直接访问敏感文件类型,并禁止目录列表
<FilesMatch "\.(php|html?|js|css|log|txt)$">
    Order Allow,Deny
    Deny from all
</FilesMatch>

# 如果上面规则导致你的图片也无法访问,可以使用更安全的规则:
# 仅允许通过WordPress(即你的域名)来访问图片等媒体文件
RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{REQUEST_URI} \.(jpg|jpeg|png|gif|pdf|doc|docx)$ [NC]
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?你的网站域名\.com/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|pdf|doc|docx)$ - [NC,F,L]

# 始终禁止目录浏览
Options -Indexes

注意:请将代码中的 你的网站域名.com 替换为你自己的实际域名(不含 http://)。这条规则的意思是:如果请求一个图片/文档,但来源不是你的网站,则直接禁止访问(403 Forbidden)。

加固步骤三:终极方案——自定义文件上传位置(进阶)
如果上述方法仍不放心,可以,将投稿文件存放到Web根目录之外。

原理:在服务器上创建一个 /home/你的账户/private_uploads/ 这类完全不在网站公开访问范围内的目录。

操作:这通常需要修改CF7或使用高级插件来重定义上传路径,并编写代码通过一个“文件代理”脚本(需验证管理员权限)来读取文件。此步骤涉及代码开发,较为复杂,除非有极高安全要求,否则完成第二步通常已足够安全。

权限隔离检查清单

为了确保彻底的隔离,请对照此清单检查网站:

检查项目目标状态(针对非管理员)如何检查/设置
1. 后台访问完全无法进入 /wp-admin用户插件(如WPCOM Member)应已将默认登录后跳转页面设为“前端用户中心”,而非后台仪表盘。
2. 前端表单可见性仅登录后可见确认投稿页面使用了 [loggedin] 等短代码包裹。
3. 媒体库访问不可见,不可用默认“投稿者”角色无“上传文件”权限,这已由角色控制。
4. 文件路径暴露绝对路径被隐藏按上文步骤二操作,保护上传目录。
5. 直接文件访问被服务器规则拦截通过 .htaccess 规则实现,同上。
6. 角色晋升严格控制绝不将普通用户提升为“编辑”或“管理员”。

 文件上传到哪里去了?(存储位置)

文件上传的存储位置访问管理是两个独立但相关的问题,而Flamingo的“设置”选项确实不够直观,由 Contact Form 7 (CF7) 上传的文件,默认存储在以下服务器路径:
/wp-content/uploads/wpcf7_uploads/

这是如何运作的?

  1. 临时存储:用户提交表单时,文件首先会传到这个目录,文件名会被系统自动重命名为一串随机字符(如 a1b2c3d4e5.pdf),这本身就隐藏了原始文件名。
  2. 记录链接Flamingo 插件在记录这次提交时,会在数据库里保存一个指向这个临时文件的链接。
  3. 自动清理:默认情况下,这些临时上传的文件会在24小时后被系统自动删除。这就是为什么必须安装Flamingo来永久保存记录的原因,它保存的是“文件曾经在此”的信息,而不是文件本身。

重要提示:如果想让文件永久保存,需要安装额外的扩展插件(如 Contact Form 7 File Download),它会将文件转移到媒体库。但目前你的免费方案中,Flamingo + 邮件通知已足够你获取文件。

如何在Flamingo中管理文件?(防止暴露)

Flamingo本身没有复杂的“文件上传”设置页面。它的核心是记录数据。你真正需要做的安全加固,是在服务器层面阻止对这个上传目录的直接访问。

操作路径如下:

  1. 找到目录:使用你的主机控制面板(如cPanel)的“文件管理器”,或FTP工具(如FileZilla),连接到你的网站。
  2. 导航到路径:依次打开 wp-content → uploads 文件夹。
  3. 确认子目录:查看里面是否有 wpcf7_uploads 文件夹。这就是CF7上传文件的确切位置。
  4. 实施保护(核心步骤):在该 wpcf7_uploads 目录下,创建或编辑一个名为 .htaccess 的文件,并放入我之前回复中的安全规则。这会阻止任何人通过直接输入网址来访问里面的文件。

简单来说,整个流程的安全逻辑是这样的:

步骤发生了什么安全状态你的控制点
1. 用户提交文件上传到 /wpcf7_uploads/,被重命名。临时、随机化通过CF7表单限制文件类型和大小。
2. 数据记录文件上传Flamingo在后台记录下这次提交和文件链接链接被记录,但文件仍是临时的只有登录WordPress后台才能看到Flamingo记录。
3. 你审核你登录后台,在Flamingo中点击该文件链接进行下载和审核。通过后台权限验证后的安全访问你是管理员,这是合规的访问途径。
4. 文件清理24小时后,服务器上的临时文件被自动删除。彻底清理如果文件重要,你应在此之前将其保存到本地。

所以,找不到Flamingo的“文件上传设置”是正常的,因为文件管理主要在服务器端。现在最有效、最直接的安全加固动作是:去你的网站服务器上,找到 /wp-content/uploads/wpcf7_uploads/ 这个目录,然后按照上文中的【加固步骤二】,创建或修改 .htaccess 文件。这样一来,即使有人从Flamingo的记录里看到了文件链接,或者试图猜测文件路径,服务器也会拒绝他的直接访问请求,只有你通过后台才能正常查看。这才是实现“只有管理员能访问”的根本方法。

当然当前仅靠 .htaccess 进行后期拦截是被动防御,是不足以实现基础的安全防护,你需要更加严格的规则,我们还面临很多挑战。比方说,他虽然将文件名后缀改的是正常的,但是呢,它利用Burp拦截这个包在这个包的请求里面,偷偷的修改了这个文件后缀这样子.又或者他上传了一个图片.但是他这个图片呢是通过图片加木马合并的而成的图片马。要主动解决这些问题,我们需要调整策略,建立一套“纵深防御”的主动处理流程,核心是 “隔离、验证、转换”。之前编写的规则逻辑存在冲突(前段拒绝所有访问,后段又尝试允许特定类型),并且无法防御图片马。

 纵深防御:主动处理流程

理想的防御流程应如下图所示,在文件被接触前就完成多重验证与无害化处理:

实现:组合使用插件与自定义代码

由于WordPress和CF7的默认功能有限,实现上图流程需要结合插件和自定义代码。

安全目标推荐实现方案具体操作与说明
1. 立即重命名并移至Web根目录外自定义代码修改CF7上传路径这是最根本的解决方案。通过一段代码,将CF7上传目录改为Web无法直接访问的位置(如 /home/your_user/private_uploads/),并强制使用随机文件名。这需要将代码添加到主题的 functions.php 文件或自定义插件中。(代码示例见下文)
2. 验证文件真实类型(防伪扩展名)服务器端MIME类型检查在上面的自定义代码中,整合PHP的 finfo_file() 函数,通过读取文件头部二进制签名来判断真实类型,与文件后缀名进行比对,严格拒绝不一致的文件。
3. 处理图片马(破坏嵌入代码)对图片进行“再处理”使用PHP的GD库或Imagick,对上传的图片进行简单的重新压缩、缩放或格式转换。这个过程会破坏图片像素层中可能隐藏的恶意代码,而基本不影响预览。这可以整合到上述代码中,作为对图片文件的专门处理。
4. 强化访问控制(.htaccess)保护临时目录(如果仍需存在)如果你暂时无法实现方案1,那么至少应修正你的规则,并保护临时目录。将 wpcf7_uploads 目录下的 .htaccess 文件内容精简并修正为以下内容,它能更安全地拒绝所有访问:
# 无条件拒绝所有直接访问
Require all denied

核心代码示例(用于 functions.php)

以下是一个基础示例代码框架,实现了重命名、转移目录和简单的MIME检查。请注意,这需要具备一定的代码编辑能力,并需要根据你的服务器环境调整路径。

add_filter( 'wpcf7_upload_dir', 'custom_wpcf7_upload_dir' );
function custom_wpcf7_upload_dir( $dir ) {
    // 1. 定义一个新的、Web无法访问的绝对路径(请修改为你的实际路径)
    $private_dir = '/home/你的服务器用户名/private_uploads/wpcf7_uploads';

    // 2. 确保该目录存在且有写入权限
    if ( ! file_exists( $private_dir ) ) {
        wp_mkdir_p( $private_dir );
    }

    // 3. 覆盖CF7默认路径
    $dir['path']   = $private_dir;
    $dir['url']    = ''; // 置空URL,使直接链接失效
    $dir['subdir'] = '';

    return $dir;
}

// 可选:添加简单的文件头检查(基础示例)
add_filter( 'wpcf7_upload_file_name', 'custom_wpcf7_randomize_name', 10, 3 );
function custom_wpcf7_randomize_name( $filename, $file_path ) {
    // 生成随机文件名并保留后缀
    $ext = pathinfo( $filename, PATHINFO_EXTENSION );
    $new_filename = uniqid() . '_' . bin2hex( random_bytes( 8 ) ) . '.' . $ext;

    // 此处可添加 finfo_file() 函数进行MIME类型检查
    // $finfo = finfo_open( FILEINFO_MIME_TYPE );
    // $real_mime = finfo_file( $finfo, $file_path );

    return $new_filename;
}

第一阶段:紧急封堵(修复 .htaccess)

当前首要任务是切断所有对上传目录的直接访问。你的规则需要简化,避免内部冲突。

  1. 定位文件:使用主机商的文件管理器或FTP工具,进入 /wp-content/uploads/wpcf7_uploads/ 目录。
  2. 创建/编辑文件:创建或清空里面的 .htaccess 文件。
  3. 写入最严格规则只保留下面三行(适用于Apache 2.4+,绝大多数现代主机都支持):
# 无条件拒绝所有访问(无论来自哪里,无论什么文件类型)
Require all denied
# 禁止目录列表(双保险)
Options -Indexes
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^preluna\.xyz [NC]
RewriteRule ^(.*)$ http://www.preluna.xyz/$1 [L,R=301]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

# 以下是新增的安全规则,必须放在 WordPress 规则区块之外
# 1. 字体文件MIME类型(你原有的,可以保留)
AddType font/woff2 .woff2
AddType font/woff .woff
AddType font/ttf .ttf
AddType application/vnd.ms-fontobject .eot
AddType image/svg+xml .svg

# 2. 核心安全规则:防止将任何文件当作脚本解析
<FilesMatch "\.(php|php5|php7|phtml|pl|py|jsp|asp|sh|cgi)$">
    # 无条件拒绝访问任何脚本文件
    Require all denied
</FilesMatch>

# 3. 特别防御:即使有 .php 后缀的图片也拒绝(防御畸形解析)
<FilesMatch "^.*\.(php\.(png|jpg|gif|jpeg))$">
    Require all denied
</FilesMatch>

一个更严谨的兼容版本

当前的版本在Apache 2.4+环境下是完美的。如果希望配置更健壮,能兼容可能存在的旧版Apache环境(2.2),并更明确地阻止某些特定文件,可以使用以下版本:

# 兼容Apache 2.2与2.4+的拒绝访问规则
<IfModule mod_authz_core.c>
  # Apache 2.4+
  Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
  # Apache 2.2
  Deny from all
</IfModule>

# 禁止目录列表
Options -Indexes

# 额外安全:明确拒绝执行脚本(即使被以其他方式上传)
<FilesMatch "\.(php|php5|php7|phtml|pl|py|jsp|asp|sh|cgi)$">
  SetHandler None
  ForceType text/plain
</FilesMatch>

立刻验证:打开浏览器,尝试访问一个已知的上传文件,例如 http://你的网站.com/wp-content/uploads/wpcf7_uploads/某个文件名,应该看到 403 Forbidden(禁止访问) 错误页。这说明第一道墙已筑起。

核心提示:此 .htaccess 规则是第一层“物理隔离”。接下来,请务必继续实施第二阶段的“核心防御” ——将文件上传路径移出Web目录并立即重命名。否则,如果攻击者利用WordPress或插件本身的某个漏洞间接调用了这些文件,仅靠 .htaccess 是无法阻止的。.htaccess 加上文件转移,两者结合才能构成坚固的防线。

第二阶段:核心防御(转移并重命名文件)

这是治本之策,让恶意文件“无处安放”且“面目全非”。我们将通过添加一段代码,修改CF7的上传行为。

操作前必备备份你的网站,特别是当前主题的 functions.php 文件。

  1. 登录WordPress后台,进入 外观 -> 主题文件编辑器
  2. 在右侧找到 functions.php,点击进行编辑。
  3. 在文件末尾的 ?> 标签之前(如果没有 ?>,就加在文件最后),添加以下代码
/**
 * 防御加固第一步:将CF7上传文件移至Web无法直接访问的目录,并强制重命名
 */
add_filter( 'wpcf7_upload_dir', 'custom_wpcf7_upload_dir' );
function custom_wpcf7_upload_dir() {
    // 【关键修改】定义一个在网站公开目录(www或public_html)之外的路径
    // 请咨询你的主机商或查看FTP根目录,确定一个像/home/你的用户名/private_uploads/的路径
    // 如果暂时不确定,可先用一个更深的Web内路径,例如:
    $private_base = WP_CONTENT_DIR . '/uploads/private_wpcf7_uploads'; // 位于wp-content内但更深

    // 确保此目录存在
    if ( ! file_exists( $private_base ) ) {
        wp_mkdir_p( $private_base );
    }

    // 返回新路径,并置空URL使直接链接失效
    return array(
        'path'   => $private_base,
        'url'    => '', // 留空,使任何直接链接失效
        'subdir' => '',
        'error'  => false,
    );
}

// 为上传文件强制生成随机文件名
add_filter( 'wpcf7_upload_file_name', 'custom_wpcf7_randomize_name', 10, 3 );
function custom_wpcf7_randomize_name( $original_filename, $file_path ) {
    // 获取原始文件后缀
    $ext = pathinfo( $original_filename, PATHINFO_EXTENSION );
    // 生成一个高强度随机文件名(uniqid + 随机字节)
    $new_filename = sprintf( '%s_%s.%s', uniqid(), bin2hex( random_bytes( 8 ) ), strtolower( $ext ) );
    return $new_filename;
}

修改代码中的路径(重要!):找到 $private_base = ... 这一行。如果你不知道绝对安全路径,暂时先使用代码中给出的默认路径WP_CONTENT_DIR . '/uploads/private_wpcf7_uploads')。这至少能把文件藏到更深目录。

验证:提交一个带文件的测试投稿。然后通过FTP去查看,文件是否被存到了 wp-content/uploads/private_wpcf7_uploads/ 目录下,并且名字变成了一长串随机字符。

第三阶段:进阶加固(验证与处理)

在第二阶段代码的基础上,我们可以增加“文件头验证”和“图片处理”功能。

A. 添加MIME类型验证(防伪后缀)
在上一段的 custom_wpcf7_randomize_name 函数里,$new_filename = ... 这行之前,可以加入检查:

// 在生成新文件名前,检查文件真实类型
$allowed_mime = array(
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'application/pdf' => 'pdf',
    'application/msword' => 'doc',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
);
// 1. 获取声称的扩展名
    $claimed_ext = strtolower( pathinfo( $original_filename, PATHINFO_EXTENSION ) );
    // 如果扩展名根本不在允许列表,直接拒绝
    if ( ! array_key_exists( $claimed_ext, $allowed_types ) ) {
        return ''; // 返回空字符串会触发CF7上传错误
    }

// 2. 获取文件的真实MIME类型(读取文件头字节签名)
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$real_mime = finfo_file( $finfo, $file_path );
finfo_close( $finfo );

// 3. 验证真实MIME类型是否与声称的扩展名匹配
    if ( ! in_array( $real_mime, $allowed_types[ $claimed_ext ] ) ) {
        // 不匹配!这是一个伪装的恶意文件
        // 可以记录日志或执行其他安全操作
        return ''; // 拒绝上传
    }
    // --- 校验通过 ---

    // 原逻辑:生成高强度随机文件名
    $new_filename = sprintf( '%s_%s.%s', uniqid(), bin2hex( random_bytes( 8 ) ), $claimed_ext );
    return $new_filename;
}

B. 处理图片马(破坏嵌入数据)
这需要更复杂的图像处理。一个相对简单的方法是强制转换图片格式和尺寸。此操作较复杂,建议在完成前两步并稳定运行后,核心思路是:使用PHP GD库或Imagick,将上传的图片重新采样、缩放、保存,这个过程会剥离所有非像素数据。(我放到后面再讲)

哎,那假设我又有一些新的攻击手段

比方说我采用1.php.png的这种形式上传,又或者1.pphphp.png的形式这样子可以继续干扰了.还有一些在互联网上为了保证执行和解析。可能在上传的时候就直接把文件名改成那种经过URL编码之后的文件名来进行绕过,所以说就单单觉得单单这样子还是不够的。

.htaccess是基于文件名和路径的“规则匹配”,而攻击者总是试图混淆、干扰或绕过这些规则.htaccess 是防御链条中的重要一环,但绝非最终防线

合并后的配置已经比之前强大,特别是 SetHandler None 和 ForceType text/plain 试图阻止脚本执行。然而,面对我提出的攻击手段:

攻击手法现在的配置能否防御?原因分析
1.php.png (扩展名混淆)基本可以<FilesMatch "\.(php...)$"> 匹配末尾的 .php1.php.png 末尾是 .png,不匹配,因此文件会被允许上传。但这本身不一定致命,因为服务器通常根据最后一个后缀.png)决定MIME类型,不会执行PHP。但这是危险的开始。
1.pphphp (解析混淆)可能被绕过这取决于服务器的解析顺序递归删除策略。有些旧版或配置不当的服务器可能会递归删除 .php 部分,最终将 1.pphphp 解析为 1.php。你的规则依赖扩展名匹配,可能无法拦截。
URL编码 (1.ph%70hp)很可能失效.htaccess 规则通常在URL解码前进行匹配。因此,它看到的是原始字符串 1.ph%70hp,不会匹配 \.(php)。服务器解码后,该文件可能被执行为 1.php这是重大绕过风险。

如何构建防御?

真正的安全不是堵一个洞,而是建立一套无论攻击者如何变换文件名,都无法危害系统的流程。这需要跳出 .htaccess 的范畴,建立四层主动防御

第一层:应用层绝对控制(治本之策)

这是我们之前讨论的核心防御,必须实施。在你的 functions.php 中添加代码,实现:

  1. 立即转移并重命名:上传后,文件立刻被移出Web根目录(如 /home/user/private_)并重命名为随机字符串(如 a1b2c3d4e5)。攻击者即使上传成功,也永远不知道文件在哪、叫什么。
  2. 验证文件“魔数”:用PHP的 finfo_file() 函数读取文件头部字节签名(如 FF D8 FF E0 代表JPEG),判断真实类型,与后缀名严格比对。这可以防御所有文件名/编码欺骗。
  3. 图片文件“再编码”:对允许的图片,用GD库或Imagick进行重新采样、压缩、保存。这会彻底破坏图片中隐藏的任何非图像数据(如图片马)。

第二层:服务器解析层加固(弥补漏洞)

在网站的主 .htaccess(位于网站根目录)或虚拟主机配置中,添加以下规则,可以防御一些服务器解析漏洞:

# 防止将图片文件当作PHP解析(防御解析漏洞)
<FilesMatch "\.(php|php5|phtml|pl|py|jsp|asp|cgi)$">
    # 无条件拒绝访问任何脚本文件
    Require all denied
</FilesMatch>

# 特别防御:即使有 .php 后缀的图片也拒绝
<FilesMatch "^.*\.(php\.(png|jpg|gif))$">
    Require all denied
</FilesMatch>

第三层:Web目录隔离

为 wpcf7_uploads 目录设置的 .htaccess (Require all denied必须保留。这是最后一道物理屏障,即使有文件因未知漏洞被放到了这里,也无法直接访问。

第四层:运维层面防护

  1. 保持所有软件更新:WordPress核心、插件、主题、PHP版本、服务器系统。
  2. 使用专业安全插件:如 Wordfence,它具备防火墙文件完整性监控功能,能主动阻断可疑请求,并在文件被篡改时报警。
  3. 最小化暴露:服务器错误页面不显示路径信息。

Nginx版本

什么你告诉我你的配置已经修改完了,但是测试的时候却没有返回效果…..

这是一个非常关键的问题,说明安全屏障没有生效。403未出现,通常意味着 .htaccess 文件未被Apache服务器正确读取或执行

第一步:最可能的原因——文件放错了位置

.htaccess 的规则只对它所在目录及其子目录生效。你必须将文件放在你想要保护的那个具体文件夹里。

  1. 请再次确认:你修改的是哪个 .htaccess 文件?你需要保护的是 wpcf7_uploads 目录。
  2. 正确路径:包含 Require all denied 规则的 .htaccess 文件,必须位于以下路径:
    /wp-content/uploads/wpcf7_uploads/.htaccess
    (请注意文件名以点开头)
  3. 检查方法:用FTP或文件管理器,导航到这个目录,确认 .htaccess 文件就在这里,而不是在网站根目录或其他地方。

第二步:检查规则本身与服务器环境

  1. 规则独立性:确保你的规则没有错误地嵌套在 <IfModule mod_rewrite.c> 或 # BEGIN WordPress 等任何其他模块区块内。在 wpcf7_uploads 目录下的 .htaccess,内容应该只有纯粹的安全规则,例如:apache# 无条件拒绝所有访问 Require all denied Options -Indexes不要包含 RewriteEngine On 等WordPress重写规则。
  2. 测试 .htaccess 是否被读取
    • 在浏览器中尝试直接访问 .htaccess 文件本身,例如:
      http://www.preluna.xyz/wp-content/uploads/wpcf7_uploads/.htaccess
    • 预期结果:应该返回 403 Forbidden 或 500 Internal Server Error。这说明服务器识别并保护了该文件,规则正在工作。
    • 错误结果:如果返回 404 Not Found,说明文件名错误(可能漏了开头的点,成了 htaccess)。如果可以直接看到文件内容,则说明服务器完全忽略此文件,这是最严重的情况。

第三步:终极可能——服务器不是Apache

如果上述所有检查都正确,但规则依然无效,最大的可能性是你的网站托管在 Nginx 服务器上。Nginx 服务器不读取 .htaccess 文件,因此所有配置都无效。

如何判断?
查看主机控制面板信息,确认服务器类型。一个间接判断方法是:如果你的网站根目录没有 .htaccess 文件也能正常运行(特别是固定链接),那么很可能用的是Nginx。

如果确认是Nginx,解决方案如下:
你需要将 .htaccess 中的规则翻译成 Nginx 的配置语法,并添加到网站的 Nginx 配置文件中(通常位于 /etc/nginx/sites-available/ 你的网站.conf`)。这通常需要联系主机商或拥有服务器权限的管理员来操作。

对应规则如下:

location ^~ /wp-content/uploads/wpcf7_uploads/ {
    # 无条件拒绝所有访问
    deny all;
    # 返回403错误
    return 403;
}

Nginx 服务器完全不读取也不理会 .htaccess 文件。在 wpcf7_uploads 目录下放置 .htaccess 或在网站根目录修改它,对 Nginx 来说都是无效的,这就是依然可以访问 .php 文件的根本原因。需要在 Nginx 的服务器配置文件中设置规则。这对于普通网站所有者来说,通常无法直接操作。

解决方案:将规则添加到 Nginx 配置中

需要将以下配置规则添加到网站的 Nginx 服务器配置块(Server Block) 中。这个文件通常位于 /etc/nginx/sites-available/ 目录下,名为您的域名(如 preluna.xyz)或以 default 命名的文件。请将以下代码段,添加到您网站Nginx配置文件中 server { ... } 大括号内的任意位置(通常放在 location / { ... } 块之前或之后)。

# === 1. 核心防护:彻底禁止访问上传目录 ===
# 此规则优先级最高,匹配 /wp-content/uploads/wpcf7_uploads/ 下的所有请求
location ~* ^/wp-content/uploads/wpcf7_uploads/ {
    deny all;
    return 403;
}

# === 2. 防止恶意脚本伪装成图片执行(防御图片马等)===
# 匹配任何试图以图片后缀结尾的PHP文件,如 shell.php.jpg
location ~* \.php\.(jpg|jpeg|png|gif|webp)$ {
    deny all;
    return 403;
}

# === 3. 防止直接访问敏感脚本文件(全局生效) ===
# 匹配任何目录下的PHP等脚本文件
location ~* \.(php|php5|php7|phtml|pl|py|jsp|asp|sh|cgi)$ {
    # 但必须放行WordPress核心的index.php和admin-ajax.php等,否则网站会瘫痪
    location ~* ^/index\.php$ { }
    location ~* /wp-admin/admin-ajax\.php$ { }
    # 除了上面放行的,其他所有脚本请求一律拒绝
    deny all;
    return 403;
}

# === 4. 禁止访问隐藏文件(如 .git、.env、.htaccess 本身) ===
location ~ /\.(?!well-known) {
    deny all;
    return 403;
}
  • 第1条规则:是最迫切需要的,它让任何试图直接访问投稿文件的请求立即返回 403 错误。
  • 第2、3、4条规则:是深度加固,防御图片马、脚本执行等高级威胁,并保护服务器配置文件。

操作后验证

配置保存后,必须执行以下命令重载 Nginx 配置才能生效:

sudo nginx -t  # 测试配置文件语法是否正确
sudo systemctl reload nginx  # 或 sudo service nginx reload (重载服务)

生效测试
在浏览器访问:http://www.preluna.xyz/wp-content/uploads/wpcf7_uploads/任意文件名
应该立即看到 “403 Forbidden” 错误。

 返回 “404 未找到” 意味着Nginx成功拦截了请求,并伪装成“该文件不存在”,这比直接返回 “403 禁止访问” 在安全上甚至更隐蔽、更友好(不给攻击者任何提示)。这通常是由配置文件中原有的另一条通用规则造成的。在配置中,有这样的规则:

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
    expires      30d;
    error_log /dev/null;
    access_log /dev/null; # 这一行是关键!
}

注意其中的 access_log /dev/null;。当新添加的安全规则 location ~* ^/wp-content/uploads/wpcf7_uploads/ 生效并 deny all; 后,Nginx会继续向下匹配其他规则。这个图片规则也匹配到了 .jpg 后缀,并执行了 access_log /dev/null;。有时,这种组合(拒绝访问+静默日志)会导致Nginx最终返回一个 404 状态码。这完全达到了一开始的安全目的。

哎,那如果我想要通过返回模糊的错误信息来迷惑攻击者呢?

这是一种非常有效的“安全混淆”策略。这与单纯返回404(资源不存在)或403(禁止访问)相比,能更有效地增加攻击者的判断成本。

要实现这种效果,核心是修改服务器配置,当访问被阻止的资源时,不返回明确的状态码,而是返回一个模棱两可的响应。

主流混淆方案对比,可以根据混淆效果和实现难度来选择:

方案核心原理效果与优点潜在缺点
返回 200 OK + 通用页面无论文件是否存在,均返回200状态码和一个空白页/误导性页面混淆效果最佳。攻击者无法通过状态码判断是成功、被拒还是路径错误。可能影响SEO(搜索引擎可能收录无效路径)。
返回 410 Gone(已删除返回410状态码,表示资源曾存在但已永久删除能有效误导攻击者,让其认为探测的路径已失效。与“404”类似,仍是一个明确的状态码。
返回 403 Forbidden(禁止访问)返回403状态码,表示资源存在但无权限。比404更具迷惑性,能让攻击者误以为触发了权限系统状态码本身仍暗示了路径“存在”。
重定向到其他页面将请求302/301重定向到首页、登录页或一个随机错误页面。直接打断攻击者的探测流程,增加其分析难度。重定向链条可能被自动化工具跟踪。

如何配置 Nginx

以下是返回 200 OK + 通用页面的配置方法,你需要将其添加到你的Nginx网站配置文件的 server 块中合适的位置(例如,放在之前添加的安全规则附近):

# 1. 定义一个用于处理“不存在路径”的通用位置块
location @generic_error {
    # 返回200状态码和一个非常简短的通用内容(或空白)
    return 200 "服务请求异常,请稍后再试。";
    # 你也可以将其指向一个内容迷惑的HTML文件
    # return 200 /path/to/confusing_page.html;
}

# 2. 在你的防护location块中,使用 try_files 并指向上述通用处理
# 修改你之前为 wpcf7_uploads 目录写的规则:
location ~* ^/wp-content/uploads/wpcf7_uploads/ {
    # 尝试访问文件,如果不存在(这是必然的),则内部跳转到 @generic_error
    try_files $uri @generic_error;
    # 同时,为了绝对安全,在此处也保留deny all指令(作为第二道防线)
    deny all;
    access_log /dev/null;
    log_not_found off; # 可选:不在错误日志中记录404,减少信息泄露[citation:3]
}

# 3. (可选)将这种混淆应用到更广泛的范围,例如所有不存在的路径
location / {
    try_files $uri $uri/ =404; # 这是默认行为,你可以改为:
    # try_files $uri $uri/ @generic_error; # 让整个网站对无效路径都返回混淆信息
}
  1. log_not_found off:这个指令非常有用,它可以阻止Nginx将“文件未找到”的错误记录到 error.log 中。这意味着攻击者即使反复探测,也不会在服务器日志中留下清晰的“404”痕迹,进一步实现了模糊化。
  2. 作用范围:以上配置主要针对 wpcf7_uploads 目录。如果你将最下方的 location / 块也修改了,那么网站所有不存在的路径(比如攻击者胡乱猜测的 /admin.php/wp-login.php)都会返回同样的混淆信息,实现全局深度防御。
  3. 内容设计return 200 后面的内容可以精心设计,例如模仿一些常见的服务端错误信息、JSON格式的乱码,或者直接是一个空白页。

如果有异常的行为,对IP进行封禁

要实现动态IP封禁和错误混淆,需要结合Nginx配置和外部工具/脚本。

一、核心配置要点澄清

需要理解并操作两类文件,它们共同构成防御体系

文件类型作用位置与名称示例
Nginx主配置文件核心防御与混淆规则:定义受保护目录(如wpcf7_uploads)、设定错误混淆、进行基础IP封禁。通常是 /etc/nginx/sites-available/ 下你的网站配置文件,或宝塔面板的网站配置。
IP封禁动态管理文件动态IP名单:存储需要动态封禁的IP列表。Nginx通过include指令实时读取。可自定义,例如 /etc/nginx/conf.d/blockips.conf 或 /etc/nginx/dynamic/blacklist.conf

二、实现方案与配置方法

下面是一个结合了所有需求(混淆、动态封禁、豁免测试IP)的综合方案。它基于Nginx + Shell脚本 + 动态黑名单文件,无需安装复杂依赖,易于管理和调试。

第一步:在Nginx中配置“错误混淆”与动态封禁框架

请将以下配置添加到你的Nginx网站配置文件中,放在 server { ... } 块内合适位置。

# 1. 定义动态IP黑名单的存放位置(非常重要!)
include /etc/nginx/conf.d/blockips.conf;

# 2. 保护特定目录并实现“错误混淆”
location ~* ^/wp-content/uploads/(wpcf7_uploads|private_wpcf7_uploads)/ {
    # 首先应用IP黑名单规则
    deny all; # 此指令会对blockips.conf中的IP生效

    # 实现混淆:无论文件是否存在,都返回“200 OK”和一个通用错误页
    # 这里用return返回纯文本,你也可以用error_page指向一个精心设计的HTML页面
    return 200 "Service Temporarily Unavailable. Please try again later.\n";
    # 可选:完全关闭此位置的日志,让攻击者无从分析
    access_log off;
    log_not_found off;
}

# 3. (关键)为你的测试IP设置白名单,防止误封
geo $is_whitelist {
    default 0;
    # 请将下面的 123.456.78.90 替换为你真实的、固定的测试公网IP
    123.456.78.90 1;
    # 可以继续添加其他可信IP,如你自己的办公网络IP
    你的其他IP 1;
}

# 4. 在全局或关键位置应用白名单逻辑
location / {
    # 如果IP不在白名单,且存在于动态黑名单中,则拒绝访问
    if ($is_whitelist = 0) {
        # 此处的deny all效果会被上方的include文件动态扩展
    }
    # ... 你的其他配置 ...
}

如果你不会的话,则就复制下面内容,并进行完全替换。

# ==================== 【第一部分:动态IP黑名单与白名单 (必须放在server块外)】 ====================
# 1. 动态黑名单:脚本或fail2ban会向这个文件写入 'deny IP;' 规则
include /etc/nginx/conf.d/blockips.conf;

# 2. IP白名单:定义变量 $is_whitelist, 在白名单IP上值为1,其他为0
# 【!!!重要修改!!!】将下面 8.8.8.8 替换为你自己的公网IP(百度搜索“IP”可查),否则你可能被封禁
geo $is_whitelist {
    default 0;
    8.8.8.8 1; # 示例,请替换。可添加多行,如:114.114.114.114 1;
}

# ==================== 【第二部分:服务器主配置】 ====================
server {
    listen 80;
    server_name www.preluna.xyz preluna.xyz;
    index index.php index.html index.htm default.php default.htm default.html;
    root /www/wwwroot/114.66.59.86;

    # ========== 【以下为面板自动生成或必需的引用配置,请勿删除】 ==========
    #CERT-APPLY-CHECK--START
    include /www/server/panel/vhost/nginx/well-known/114.66.59.86.conf;
    #CERT-APPLY-CHECK--END

    #SSL-START
    #error_page 404/404.html;
    #SSL-END

    #ERROR-PAGE-START
    error_page 404 /404.html;
    #ERROR-PAGE-END

    #PHP-INFO-START
    include enable-php-80.conf;
    #PHP-INFO-END

    #REWRITE-START
    include /www/server/panel/vhost/rewrite/114.66.59.86.conf;
    #REWRITE-END
    # ========== 【面板生成内容结束】 ==========

    # ==================== 【第三部分:基础安全与静态文件规则】 ====================
    # 1. 禁止访问敏感系统文件
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
        return 404;
    }

    # 2. SSL证书验证目录(Let's Encrypt等需要)
    location ~ \.well-known {
        allow all;
    }

    # 3. 防止在证书验证目录上传脚本
    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    # 4. 字体文件缓存优化
    location ~* \.(woff2|woff|ttf|eot|svg)$ {
        types {
            font/woff2 woff2;
            font/woff woff;
            font/ttf ttf;
            application/vnd.ms-fontobject eot;
            image/svg+xml svg;
        }
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # ==================== 【第四部分:核心安全防护规则 (按优先级从高到低)】 ====================
    # 【规则A】保护上传目录 - 实现“混淆响应”
    # 作用:任何直接访问投稿文件夹的请求,无论文件是否存在,都返回200和混淆文本,让攻击者无法判断。
    location ~* ^/wp-content/uploads/(wpcf7_uploads|private_wpcf7_uploads)/ {
        deny all; # 动态黑名单在此生效
        return 200 "Service Temporarily Unavailable. Please try again later.\n";
        access_log off; # 不记录访问日志,增加隐蔽性
        log_not_found off; # 不记录“未找到”错误
    }

    # 【规则B】防止恶意脚本伪装成图片/文档(如 shell.php.jpg)
    location ~* \.php\.(jpg|jpeg|png|gif|webp|pdf|doc|docx)$ {
        deny all;
        return 403;
    }

    # 【规则C】防止直接访问敏感脚本文件(全局拦截)
    # 作用:拦截所有对php等脚本的访问,但放行WordPress核心文件(index.php, admin-ajax.php)。
    location ~* ^/(?!index\.php|wp-admin/admin-ajax\.php).*\.(php|php5|php7|phtml|pl|py|jsp|asp|sh|cgi)$ {
        deny all;
        return 403;
    }

    # ==================== 【第五部分:通用静态文件缓存规则 (优先级最低)】 ====================
    # 注意:前面的安全规则匹配后,就不会走到这里。只有正常图片/js/css才会由这里处理。
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
        expires 30d;
        error_log /dev/null;
        access_log /dev/null;
    }

    location ~ .*\.(js|css)?$ {
        expires 12h;
        error_log /dev/null;
        access_log /dev/null;
    }

    # ==================== 【第六部分:日志配置】 ====================
    access_log  /www/wwwlogs/114.66.59.86.log;
    error_log  /www/wwwlogs/114.66.59.86.error.log;
}
# ==================== 【配置文件结束】 ====================

第二步:创建并管理动态IP黑名单

我们通过一个脚本和黑名单文件来实现IP的动态封禁与解封。

1.创建黑名单文件:这个文件初始为空。脚本会向其中写入 deny IP地址; 格式的规则

sudo touch /etc/nginx/conf.d/blockips.conf

2.创建管理脚本:创建一个脚本(如 /usr/local/bin/manage_blockip.sh),用于添加/删除封禁IP,并自动重载Nginx。

#!/bin/bash
BLOCK_FILE="/etc/nginx/conf.d/blockips.conf"

# 添加封禁IP
block_ip() {
    if ! grep -q "deny $1;" "$BLOCK_FILE"; then
        echo "deny $1;" >> "$BLOCK_FILE"
        echo "已封禁 IP: $1"
    else
        echo "IP: $1 已在黑名单中"
    fi
}

# 解除封禁IP
unblock_ip() {
    sed -i "/deny $1;/d" "$BLOCK_FILE"
    echo "已解封 IP: $1"
}

# 重载Nginx配置
reload_nginx() {
    if nginx -t > /dev/null 2>&1; then
        systemctl reload nginx  # 或 sudo service nginx reload
        echo "Nginx 配置已重载"
    else
        echo "Nginx 配置测试失败,请检查语法"
        exit 1
    fi
}

# 使用示例
case $1 in
    "add")
        block_ip $2
        reload_nginx
        ;;
    "remove")
        unblock_ip $2
        reload_nginx
        ;;
    "list")
        cat "$BLOCK_FILE"
        ;;
    *)
        echo "用法: $0 {add|remove|list} [IP地址]"
        exit 1
        ;;
esac

给脚本执行权限:sudo chmod +x /usr/local/bin/manage_blockip.sh

3.使用脚本管理IP

  • 封禁IPsudo manage_blockip.sh add 攻击者IP
  • 解封IPsudo manage_blockip.sh remove 攻击者IP
  • 查看列表sudo manage_blockip.sh list

 一个极其重要的步骤:设置你的白名单IP

在测试前,必须将 geo 块里的示例 IP 123.456.78.90 替换成你当前操作电脑的公网IP(你用来SSH连接或管理网站的IP)。你可以通过访问 ipinfo.io 快速获取。

如果你不替换,后续的任何封禁测试都可能把你自己的IP封掉,导致你无法访问自己的网站!

我通过百度搜索IP获得,他给我返回的是223.这个开头的IP,那为什么我直接在我电脑上通过IP命令,获得的那个地址,它不属于公网IP。当从不同渠道获取的IP不一致时,原因在于你家里的网络是 “多人共享一个出口” 的结构。

用一个比喻来解释:想象你住在一栋公寓楼里。

  • 你电脑上查到的IP(比如 192.168.1.101)就像是你的 “房间号” 。这个号码只在公寓楼内部使用,邮递员无法直接把信送到这个“房间号”。
  • 百度搜索出来的IP223.xxx.xxx.xxx)就像是这栋公寓楼的 “大楼地址” 。所有寄给楼里住户的信件,都会先送到这个大楼地址,再由管理员(路由器)根据房间号分发给每个人。

工作流程:当你的电脑(192.168.1.101)访问我的服务器时,数据包会先经过你的路由器。路由器会做一个“地址转换”(NAT),把发出地址从你的内网IP(192.168.1.101:1234)替换成你家网络的公网IP(223.xxx.xxx.xxx:5678)再发出去。我的服务器只能看到并回复到你的公网IP(223.xxx.xxx.xxx:5678),然后由你的路由器根据端口号5678再把数据准确转发回你的电脑。这就是为什么Nginx白名单必须填公网IP:因为当你的请求到达服务器时,Nginx看到的来源IP就是你查询到的那个 223.xxx.xxx.xxx,而不是你电脑内部的 192.168.1.101。很多家庭宽带运营商为了节省IPv4地址,会使用“运营商级NAT”(Carrier-Grade NAT)。这意味着你从百度查到的那个223开头的IP,可能不是你独享的,而是和同一区域的其他很多用户共享的

如何简单判断?

登录你家路由器的管理后台(通常地址是 192.168.1.1 或 192.168.0.1),在“WAN口状态”或“网络信息”里查看获取到的IP地址。如果路由器里显示的IP(比如10.x.x.x100.x.x.x和百度查到的不同,那就说明你的网络外层还有一层运营商的NAT。这种情况下,你配置白名单的效果会打折扣,因为和你共享这个公网IP的其他用户的行为也可能影响你。如果这个IP和你在百度查到的 完全一致,那么恭喜,你拥有独立的公网IP,配置白名单最有效。

在共享IP环境下,基于WordPress用户角色和登录状态的“分级响应与监控”

将安全重心从可能无效的IP封禁,转移到了网站应用层面。

在共享IP环境下,以用户权限为核心,在WordPress应用层内建立主动防御体系。

唉不是,这插件太贵了,根本用不起一年。
兄弟们,这并不好笑,所以我们就想着我们自己去编写一个代码或者插件,去完成这个上述的功能。

functions.php 文件方法

代码模块一:管理员豁免与员工实时封禁,将以下代码添加到你的主题 functions.php 文件末尾,或创建一个简单的自定义插件。请务必修改其中提到的路径和邮箱

/**
 * 主动防御核心模块:监控员工操作并实时封禁
 */
add_action('admin_init', 'custom_monitor_employee_actions');
function custom_monitor_employee_actions() {
    // 1. 获取当前用户
    $current_user = wp_get_current_user();
    
    // 2. 核心:如果是管理员,直接放行,不做任何检查
    if (in_array('administrator', (array) $current_user->roles)) {
        return; // 管理员拥有最高豁免权
    }
    
    // 3. 定义员工的敏感禁区(URL关键词,可按需增删)
    $restricted_paths = array(
        'plugin-install.php',
        'theme-install.php',
        'users.php',
        'tools.php',
        'options-general.php',
        '/wp-content/plugins/',
        '/wp-content/themes/'
    );
    
    $current_uri = $_SERVER['REQUEST_URI'];
    
    // 4. 检查员工是否踏入禁区
    foreach ($restricted_paths as $path) {
        if (strpos($current_uri, $path) !== false) {
            // 5. 触发封禁流程
            // a. 记录到审计日志(如果WP Security Audit Log已安装)
            do_action('custom_security_alert', '员工越权访问', $current_user->user_login, $current_uri);
            
            // b. 强制该用户登出
            wp_logout();
            
            // c. 将该用户角色降级为“无”,使其账号失效(关键步骤)
            $current_user->set_role(''); // 设置为空角色,即失去所有权限
            
            // d. 重定向到带有警告信息的登录页
            wp_redirect(wp_login_url() . '?action=blocked&reason=unauthorized');
            exit;
        }
    }
}

/**
 * (可选)向管理员发送邮件通知
 */
add_action('custom_security_alert', 'custom_send_security_email', 10, 3);
function custom_send_security_email($alert_type, $username, $detail) {
    $admin_email = get_option('admin_email'); // 你的邮箱
    $subject = '【安全警报】您的网站有异常操作';
    $message = "警报类型:{$alert_type}\n触发用户:{$username}\n操作详情:{$detail}\n时间:" . current_time('mysql');
    wp_mail($admin_email, $subject, $message);
}

代码模块二:访客高频访问限制(轻量级),这个模块不需要依赖IP白名单,而是在应用层限制高频请求。将以下代码也添加到 functions.php

/**
 * 访客高频访问限制
 */
add_action('init', 'custom_rate_limit_guests');
function custom_rate_limit_guests() {
    // 只针对未登录的访客
    if (is_user_logged_in()) {
        return;
    }
    
    $visitor_ip = $_SERVER['REMOTE_ADDR'];
    $transient_key = 'rate_limit_' . $visitor_ip;
    $request_count = get_transient($transient_key);
    
    // 设置阈值:15秒内超过10次请求
    if ($request_count && $request_count > 10) {
        // 返回429状态码(请求过多),并终止执行
        status_header(429);
        exit('请求过于频繁,请稍后再试。');
    }
    
    // 增加计数,并设置15秒过期时间
    if ($request_count === false) {
        set_transient($transient_key, 1, 15);
    } else {
        set_transient($transient_key, $request_count + 1, 15);
    }
}

插件方法

第一步:创建插件基本结构

在你的网站服务器上,进入 wp-content/plugins/ 目录,创建一个新的文件夹,例如 preluna-security-guard

在该文件夹内,创建以下几个文件,整个插件的结构如下:

preluna-security-guard/
├── preluna-security-guard.php      # 插件主文件
├── includes/
│   ├── class-employee-monitor.php    # 员工监控与封禁模块
│   └── class-guest-limiter.php       # 访客频率限制模块
└── uninstall.php                   # 插件卸载清理脚本(可选)

第二步:编写插件主文件

主文件是插件的“身份证”和“总控中心”。编辑 preluna-security-guard.php,写入以下代码:

<?php
/**
 * Plugin Name:       Preluna Security Guard
 * Plugin URI:        https://www.preluna.xyz/
 * Description:       一个轻量级的WordPress主动防御插件,实现基于角色的实时监控与封禁。
 * Version:           1.0.0
 * Author:            Your Name
 * License:           GPL v2 or later
 * Text Domain:       preluna-sg
 */

// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// 定义插件路径常量,便于其他地方引用
define( 'PSG_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

// 包含核心功能类文件
require_once PSG_PLUGIN_DIR . 'includes/class-employee-monitor.php';
require_once PSG_PLUGIN_DIR . 'includes/class-guest-limiter.php';

/**
 * 初始化插件类
 */
function psg_initialize() {
    // 初始化员工监控模块
    new PSG_Employee_Monitor();
    // 初始化访客限制模块
    new PSG_Guest_Limiter();
}
add_action( 'plugins_loaded', 'psg_initialize' );

/**
 * 插件激活时做的操作(可选)
 */
function psg_plugin_activation() {
    // 可以在这里初始化一些选项,或检查依赖
    if ( ! get_option( 'psg_settings' ) ) {
        add_option( 'psg_settings', array(
            'employee_alert_email' => get_option( 'admin_email' ),
            'rate_limit_threshold' => 10,
            'rate_limit_window'    => 15,
        ) );
    }
}
register_activation_hook( __FILE__, 'psg_plugin_activation' );

/**
 * 插件停用时做的操作(可选)
 */
function psg_plugin_deactivation() {
    // 清理插件生成的临时数据(如访客频率限制的瞬态数据)
    // 注意:不要清理设置,以便重新启用时恢复
}
register_deactivation_hook( __FILE__, 'psg_plugin_deactivation' );

第三步:编写核心功能模块

这是插件的“肌肉”,我们分两个类来实现。

1. 员工监控与封禁模块 (includes/class-employee-monitor.php)

<?php
class PSG_Employee_Monitor {
    
    // 1. 定义需要监控的敏感路径(可按需修改)
    private $restricted_url_patterns = array(
        // 系统核心文件
        '/wp-admin\/(plugin-install|theme-install|users|tools|options-general|export|import)\.php/' => true,
        // 插件、主题目录探测
        '/(wp-content\/plugins\/).*\.(php|txt|sql|zip)/' => true,
        '/(wp-content\/themes\/).*\.(php|txt|sql|zip)/' => true,
        // 你之前特别保护的投稿目录(从Nginx规则移植)
        '/(wp-content\/uploads\/(wpcf7_uploads|private_wpcf7_uploads))/' => true,
    );
    
    public function __construct() {
        // 记录插件类已初始化
        error_log('PSG_DEBUG: PSG_Employee_Monitor 类已加载。');
        // 挂载监控函数到admin_init钩子
        add_action('admin_init', array($this, 'monitor_author_actions'));
    }
    
    public function monitor_author_actions() {
        $current_user = wp_get_current_user();
        $current_user_roles = (array) $current_user->roles;
        
        error_log('PSG_DEBUG: 开始检查用户。用户名: ' . $current_user->user_login . ', 角色: ' . implode(', ', $current_user_roles));
        
        // 2. 核心豁免逻辑:管理员和编辑完全放行
        if ( in_array('administrator', $current_user_roles) || in_array('editor', $current_user_roles) ) {
            error_log('PSG_DEBUG: 用户是管理员或编辑,豁免所有检查。');
            return;
        }
        
        // 3. 核心打击逻辑:仅当用户是“作者”时,才进行检查
        if ( !in_array('author', $current_user_roles) ) {
            error_log('PSG_DEBUG: 用户不是作者,结束检查。');
            return; // 如果不是作者,也结束检查(例如订阅者)
        }
        
        error_log('PSG_DEBUG: 用户是作者,开始进行敏感路径检查。');
        
        $current_uri = $_SERVER['REQUEST_URI'];
        error_log('PSG_DEBUG: 当前请求URI: ' . $current_uri);
        
        // 4. 检查当前访问的路径是否在敏感列表中
        foreach ($this->restricted_url_patterns as $pattern => $_) {
            if (preg_match($pattern, $current_uri)) {
                error_log("PSG_ALERT: 作者 {$current_user->user_login} 触发敏感路径规则。模式: {$pattern}");
                $this->block_user($current_user, 'url_access', $pattern);
                return; // 触发后立即拦截,停止后续检查
            }
        }
        error_log('PSG_DEBUG: 作者未触发任何敏感路径规则。');
    }
    
    private function block_user($user, $block_type, $trigger) {
        error_log("PSG_BLOCK: 开始执行封禁流程,用户: {$user->user_login}");
        
        // 5. 发送邮件通知
        $settings = get_option('psg_settings');
        $to = $settings['employee_alert_email'] ?? get_option('admin_email');
        $subject = '【安全警报】作者尝试进行未授权操作';
        $message = sprintf(
            "用户名:%s (ID: %d)\n触发类型:%s\n触发内容:%s\n时间:%s\nIP地址:%s\n\n该用户已被强制下线并禁用。",
            $user->user_login,
            $user->ID,
            $block_type,
            $trigger,
            current_time('mysql'),
            $_SERVER['REMOTE_ADDR']
        );
        wp_mail($to, $subject, $message);
        
        // 6. 关键封禁操作
        // a. 移除用户所有角色(使其账号失效)
        $user->set_role('');
        error_log('PSG_BLOCK: 用户角色已清空。');
        
        // b. 强制用户登出
        wp_logout();
        error_log('PSG_BLOCK: 用户已被登出。');
        
        // c. 重定向到安全提示页面
        wp_redirect(home_url('/?action=blocked&reason=unauthorized_author'));
        exit;
    }
}
?>

2. 访客频率限制模块 (includes/class-guest-limiter.php)

<?php
/**
 * 访客访问频率限制类
 * 正确的类名应为 PSG_Guest_Limiter
 */
class PSG_Guest_Limiter {
    
    public function __construct() {
        // 在 WordPress 初始化时启动频率检查
        add_action( 'init', array( $this, 'limit_requests' ) );
        error_log('PSG_DEBUG: PSG_Guest_Limiter 类已加载。'); // 调试用
    }
    
    public function limit_requests() {
        // 只针对未登录的访客
        if ( is_user_logged_in() ) {
            return;
        }
        
        $settings = get_option( 'psg_settings' );
        $threshold = $settings['rate_limit_threshold'] ?? 10;
        $window    = $settings['rate_limit_window'] ?? 15;
        
        $visitor_ip = $_SERVER['REMOTE_ADDR'];
        // 使用更短的键名,并加盐,避免冲突
        $transient_key = 'psg_rl_' . md5( $visitor_ip . 'preluna_salt' );
        $request_count = get_transient( $transient_key );
        
        if ( $request_count && $request_count >= $threshold ) {
            status_header( 429 );
            exit( '<h1>429 请求过多</h1><p>您的访问过于频繁,请等待' . $window . '秒后再试。</p>' );
        }
        
        if ( $request_count === false ) {
            set_transient( $transient_key, 1, $window );
        } else {
            set_transient( $transient_key, $request_count + 1, $window );
        }
    }
}
?>

第四步:安装、调试与测试

  1. 安装插件:将整个 preluna-security-guard 文件夹通过FTP或文件管理器上传到 wp-content/plugins/
  2. 激活插件:在WordPress后台的“插件”页面,找到 “Preluna Security Guard” 并激活它。
  3. 调试模式:在调试时,强烈建议在你的 wp-config.php 文件中开启 WordPress 调试模式,这样任何错误都会显示出来
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // 将错误记录到 wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // 不要在页面上显示错误,防止攻击者看到

进行测试:按照之前的测试流程,分别测试管理员豁免员工封禁访客限制功能。观察 debug.log 文件(如果开启)和服务器错误日志,排查问题。

“作者”能做什么 vs 插件会阻止什么

我们的插件拦截是基于 “是否访问了预设的敏感后台路径”,这与希望作者能正常使用的功能(写文章、上传图片)是两套完全独立的路径

作者角色的正常工作路径我们的插件会阻止的敏感路径示例 (在 $restricted_url_patterns 中定义)
wp-admin/post-new.php (撰写新文章)wp-admin/plugin-install.php (安装插件)
wp-admin/post.php?action=edit (编辑自己的文章)wp-admin/theme-install.php (安装主题)
wp-admin/upload.php (媒体库,上传图片)wp-admin/users.php (管理所有用户)
wp-admin/admin-ajax.php (前台异步操作,如上传)wp-admin/tools.php, wp-admin/options-general.php (网站工具和设置)
访问自己的个人资料页任何试图直接访问插件、主题目录下.php等源文件的行为
查看前台网站访问设置的特殊投稿目录 wp-content/uploads/wpcf7_uploads/

作者所有正常的内容创作和管理功能(左侧菜单栏常规项)都不会被影响。插件拦截的是插件/主题管理、用户管理、系统设置、服务器文件探测等明显超出作者权限的“系统管理”行为。

如何精确验证“功能不受影响”

验证项目操作 (使用你的作者测试账号)期望结果
1. 正常功能验证1. 进入“文章” -> “写文章”,编辑并保存。
2. 进入“媒体” -> “添加新媒体”,上传一张图片。
3. 编辑一篇自己已发布的文章。
全部成功,流程畅通无阻。
2. 越权行为验证1. 在浏览器地址栏手动输入:/wp-admin/plugin-install.php 并访问。
2. 尝试访问:/wp-admin/themes.php
3. 访问:/wp-admin/users.php
全部被拦截,账号被强制退出并禁用。
3. 邮件与日志验证完成第2步后,检查:
1. 你的管理员邮箱是否收到封禁通知邮件。
2. 插件是否在 error_log 中留下记录。
收到邮件,并且日志中应有 PSG_ALERT 等记录。

如何获取更清晰的插件运行日志

提供的日志里没有我们插件的 PSG_DEBUG 信息,可能是因为日志被其他警告淹没,或者输出被缓冲了。我们换一种更直接、独立于 WP_DEBUG 的日志方式,确保能看到插件的每一步。在插件主文件 preluna-security-guard.php 的末尾,?> 标签前,添加这个专用日志函数:

/**
 * 专用的插件日志函数,直接写入固定文件,不受其他错误干扰
 */
function psg_log( $message ) {
    $log_file = WP_CONTENT_DIR . '/psg-debug.log'; // 日志保存在 /wp-content/psg-debug.log
    $time = date( 'Y-m-d H:i:s' );
    $message = "[{$time}] {$message}" . PHP_EOL; // PHP_EOL是换行符
    // 写入文件,FILE_APPEND表示追加而不覆盖
    file_put_contents( $log_file, $message, FILE_APPEND );
}

然后,在 class-employee-monitor.php 文件中,将所有 error_log(‘PSG_DEBUG: ...’) 的语句替换为 psg_log( ‘...’ )。例如:

// 替换前
error_log('PSG_DEBUG: PSG_Employee_Monitor 类已加载。');
// 替换后
psg_log('PSG_DEBUG: PSG_Employee_Monitor 类已加载。');

构建一个可自定义的敏感资源防护体系

功能不局限于此,因为网站还有很多其他的敏感文件,所以想对这个裁剪的内容再进行扩展,比方说可以创造一个像其他插件一样那样设置的界面,只需要输入一个目录或者文件夹,那么只要是在这个目录或者文件夹下面的所有内容,他都不可以访问或者修改,但是可能掉很多个辅助功能来进行相互调用,就是比方说我正常用户去查看文章引用了一些图片,我们需要可以可选择式的去选择哪些文件列表是受影响的。或者说通过正常打开网站,正常的调用的话是不会触发的,那如果让他直接去访问这个地址或者URL的话是要收到啊屏蔽的,或者它采用了什么路径穿越?或者是返回上级目录,然后再这样进行反复跳转的,或者是路径拼接的这些,我们需要考虑一下。这是一个从“固定规则”到“灵活策略”的升级。完全可以在现有的 Preluna Security Guard 插件基础上,创建一个设置页面并实现这套逻辑。

核心思路是:在插件后台创建一个设置页,让你能自由添加需要保护的“禁区”。当有访问请求时,插件会进行智能判断,只有“直接恶意访问”才会被拦截,而网站自身的正常调用则放行。

实现步骤

我们分两步走:先为插件增加一个设置页面,然后实现上图的智能检测逻辑。

第一步:创建插件设置页面

在插件目录 (preluna-security-guard/includes/) 下新建一个文件 class-settings-page.php,并写入以下代码。它将为插件添加一个“安全禁区”配置页。

<?php
class PSG_Settings_Page {
    private $option_name = 'psg_protected_paths';
    private $settings_group = 'psg-settings-group';
    
    public function __construct() {
        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_action('admin_init', array($this, 'register_settings'));
    }
    
    public function add_admin_menu() {
        // 在“设置”主菜单下添加子菜单
        add_options_page(
            'PSG 安全禁区设置', // 页面标题
            'PSG安全禁区',      // 菜单标题
            'manage_options',   // 权限要求(管理员)
            'psg-protected-paths', // 页面URL slug
            array($this, 'render_settings_page') // 渲染页面的回调函数
        );
    }
    
    public function register_settings() {
        register_setting($this->settings_group, $this->option_name);
        add_settings_section('psg_main_section', '管理受保护的路径', null, 'psg-protected-paths');
        add_settings_field('psg_paths_field', '受保护的路径', array($this, 'render_paths_field'), 'psg-protected-paths', 'psg_main_section');
    }
    
    public function render_paths_field() {
        $paths = get_option($this->option_name, '');
        echo '<textarea name="' . $this->option_name . '" rows="10" cols="100" placeholder="每行一个路径,例如:
/wp-content/secret-files/
/wp-admin/export.php
/uploads/private/
">' . esc_textarea($paths) . '</textarea>';
        echo '<p class="description">每一行代表一个需要保护的目录或文件路径。当这些路径被<strong>直接访问</strong>时,将触发防护规则。</p>';
    }
    
    public function render_settings_page() {
        ?>
        <div class="wrap">
            <h1>PSG - 安全禁区配置</h1>
            <form method="post" action="options.php">
                <?php
                settings_fields($this->settings_group);
                do_settings_sections('psg-protected-paths');
                submit_button();
                ?>
            </form>
            <hr>
            <h3>说明</h3>
            <ul>
                <li><strong>路径格式</strong>: 以网站根目录为起点,如 <code>/wp-content/plugins/my-plugin/secret.log</code></li>
                <li><strong>智能防护</strong>: 通过页面内链接、图片src等<strong>正常调用</strong>不会触发拦截,只有浏览器地址栏直接输入、扫描器访问等才会被阻止。</li>
                <li><strong>路径规范</strong>: 系统会自动处理 <code>../</code> 等路径穿越尝试。</li>
            </ul>
        </div>
        <?php
    }
    
    // 提供一个公共方法,让其他类(如监控类)能获取到所有受保护的路径
    public static function get_protected_paths() {
        $option = get_option('psg_protected_paths', '');
        if (empty($option)) {
            return array();
        }
        // 按行分割,移除空行和空格,返回数组
        return array_filter(array_map('trim', explode("\n", $option)));
    }
}
?>

第二步:升级监控类,实现智能检测

我们需要大幅修改 includes/class-employee-monitor.php 的逻辑,使其集成新的“禁区”设置,并加入智能判断。请用以下代码完全替换原文件内容

<?php
class PSG_Employee_Monitor {
    
    public function __construct() {
        psg_log('PSG_DEBUG: 智能监控类已加载。');
        // 同时监控后台页面和所有前端请求
        add_action('admin_init', array($this, 'monitor_author_actions'));
        add_action('init', array($this, 'monitor_all_requests')); // 新增:监控所有请求
    }
    
    public function monitor_author_actions() {
        // ... (这里保留你之前写的、专门检查后台敏感页面的代码逻辑,此处省略以节省篇幅)
        // 你可以将旧的、检查固定后台路径的逻辑完全保留在这里。
    }
    
    /**
     * 新增:监控所有请求,应用“智能禁区”规则
     */
    public function monitor_all_requests() {
        // 1. 获取当前请求的完整URL和路径
        $request_uri = $_SERVER['REQUEST_URI'];
        $request_path = parse_url($request_uri, PHP_URL_PATH); // 提取路径部分
        
        // 2. 获取管理员配置的所有“禁区”路径
        $protected_paths = PSG_Settings_Page::get_protected_paths();
        if (empty($protected_paths)) {
            return; // 没有设置禁区,直接退出
        }
        
        // 3. 对请求路径进行规范化,防御路径穿越攻击 (如 /secret/../admin/)
        $normalized_request_path = $this->normalize_path($request_path);
        
        // 4. 检查请求路径是否匹配任何一个“禁区”
        $matched_protected_path = null;
        foreach ($protected_paths as $protected_path) {
            $protected_path = trim($protected_path);
            if (empty($protected_path)) continue;
            
            // 规范化保护区路径,并确保它以斜杠开头
            $normalized_protected_path = $this->normalize_path($protected_path);
            
            // 进行匹配判断:请求路径是否以保护区路径开头?
            if (strpos($normalized_request_path, $normalized_protected_path) === 0) {
                $matched_protected_path = $protected_path;
                break; // 匹配到一个就跳出循环
            }
        }
        
        // 如果没有匹配到任何禁区,直接放行
        if ($matched_protected_path === null) {
            return;
        }
        
        psg_log("PSG_DEBUG: 请求路径匹配到禁区。请求: {$request_path}, 禁区: {$matched_protected_path}");
        
        // 5. 智能判断:是否是“直接恶意访问”?
        if ($this->is_malicious_direct_access()) {
            // 认定为恶意访问,执行拦截
            $this->handle_violation($matched_protected_path, $request_path);
        } else {
            // 认定为正常引用(如图片、JS/CSS),放行
            psg_log("PSG_DEBUG: 请求来自正常页面引用,已放行。");
        }
    }
    
    /**
     * 智能判断核心:区分正常引用与直接访问
     */
    private function is_malicious_direct_access() {
        // 关键检查1:HTTP Referer 来源
        // 如果请求来自你网站内部的页面(如图片被文章引用),Referer会包含你的域名
        $referer = $_SERVER['HTTP_REFERER'] ?? '';
        $site_host = parse_url(home_url(), PHP_URL_HOST);
        
        if (!empty($referer) && stripos($referer, $site_host) !== false) {
            // Referer存在且来自本站,很可能是正常的内容引用(如图片、附件)
            return false;
        }
        
        // 关键检查2:常见的静态文件请求头
        // 许多扫描器、恶意请求的User-Agent具有特征,可以简单判断
        $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
        $suspicious_agents = array('sqlmap', 'acunetix', 'nessus', 'nikto', 'wpscan', 'dirb');
        foreach ($suspicious_agents as $agent) {
            if (stripos($user_agent, $agent) !== false) {
                return true; // 认为是恶意扫描器
            }
        }
        
        // 如果没有明确证据是正常引用,则倾向于拦截(严格模式)
        // 如果你希望更宽松,可以将这里改为 return false;
        return true;
    }
    
    /**
     * 处理违规访问
     */
    private function handle_violation($protected_path, $request_path) {
        $current_user = wp_get_current_user();
        $client_ip = $_SERVER['REMOTE_ADDR'];
        
        // 记录详细的违规日志
        psg_log("PSG_ALERT: 检测到对禁区的直接访问!路径: {$request_path}, 用户: {$current_user->user_login}, IP: {$client_ip}");
        
        // 根据用户角色执行不同操作
        if ($current_user->exists() && in_array('author', (array) $current_user->roles)) {
            // 如果是作者,执行封禁
            $this->block_user($current_user, 'direct_access_to_protected_path', $protected_path);
        } else {
            // 如果是访客或其他未授权用户,可以返回429或混淆信息
            // 这里以返回429为例,你也可以改成之前Nginx那样的混淆信息
            status_header(429);
            die('<h1>访问违规</h1><p>禁止直接访问受保护的资源。</p>');
        }
    }
    
    /**
     * 规范化路径,防御路径穿越攻击
     * 例如将 /a/b/../c 转换为 /a/c
     */
    private function normalize_path($path) {
        // 确保路径以斜杠开头,便于后续处理
        if (strpos($path, '/') !== 0) {
            $path = '/' . $path;
        }
        // 使用 realpath 的思路,但不依赖文件实际存在,仅处理 `..` 和 `.`
        $parts = explode('/', $path);
        $result = array();
        foreach ($parts as $part) {
            if ($part == '' || $part == '.') continue;
            if ($part == '..') {
                array_pop($result); // 遇到..,则回退一级
            } else {
                $result[] = $part;
            }
        }
        return '/' . implode('/', $result);
    }
    
    // 保留你原来的 block_user 方法,用于封禁作者
    private function block_user($user, $block_type, $trigger) {
        // ... (这里是你原来发送邮件、清空角色、强制下线的代码,保持不变)
    }
}
?>

第三步:整合与激活新模块

最后,我们需要在插件主文件 preluna-security-guard.php 中,引入并初始化这个新的设置页面类。在现有代码中找到包含其他类文件的地方,添加如下行:

// 包含核心功能类文件
require_once PSG_PLUGIN_DIR . 'includes/class-employee-monitor.php';
require_once PSG_PLUGIN_DIR . 'includes/class-guest-limiter.php';
// +++ 新增:引入设置页面类 +++
require_once PSG_PLUGIN_DIR . 'includes/class-settings-page.php';

然后在 psg_initialize() 函数中初始化它:

function psg_initialize() {
    new PSG_Employee_Monitor();
    new PSG_Guest_Limiter();
    // +++ 新增:初始化设置页面 +++
    new PSG_Settings_Page();
}

使用与测试指南

  1. 更新插件:将上述三个新文件上传并修改主文件后,在后台重新启用插件。
  2. 配置禁区:进入 “设置” -> “PSG安全禁区”,在文本框中每行添加一个你想保护的路径,例如:
/wp-content/uploads/secret-docs/
/wp-config-backup.txt
/inc/database-connection.php

基于角色的多层次、严格权限模型

为了实现 “不同角色有不同禁区,且高权限者也不能越界访问低权限专属区” 的严格模型,我们需要重构插件的设计。

 新模型设计:基于角色的最小权限等级

我们将引入一个 “权限等级” 概念,并为每个角色和每条“禁区”路径配置等级。

角色 (Role)权限等级 (Level)设计说明
管理员 (Administrator)10系统最高权限,但我们赋予其一个“等级”而非“万能钥匙”。
编辑 (Editor)7权限低于管理员,但高于作者。
作者 (Author)5核心创作角色,权限明确且需严格控制。
投稿者 (Contributor)3核心创作角色,权限明确且需严格控制。
订阅者/访客 (Subscriber/Visitor)1未登录访客的权限等级最低,只能访问公开资源。

核心规则:访问一条路径时,用户的权限等级必须大于或等于路径要求的“最小权限等级”。否则,无论用户等级多高,都将被拦截。例如:

  • 一个要求等级为 5(作者) 的目录,等级 7 的编辑和等级 10 的管理员访问也会被拒。
  • 一个要求等级为 1(公开) 的目录,所有用户均可访问。

实现方案:升级插件

我们需要对插件进行三处核心改造:数据结构设置界面 和 监控逻辑

第一步:更新数据结构(修改 psg_plugin_activation

在插件主文件 preluna-security-guard.php 中找到 psg_plugin_activation 函数,修改默认设置,为路径增加 min_level 字段。

function psg_plugin_activation() {
    if ( ! get_option( 'psg_protected_paths' ) ) {
        // 新的数据结构:每条记录包含 ‘path‘ 和 ‘min_level‘
        $default_paths = array(
            array( 'path' => '/wp-content/uploads/secret-admin/', 'min_level' => 10 ),
            array( 'path' => '/wp-content/uploads/author-only-uploads/', 'min_level' => 5 ),
            array( 'path' => '/wp-admin/export.php', 'min_level' => 7 ),
        );
        add_option( 'psg_protected_paths', $default_paths );
    }
}

第二步:重建设置页面(更新 class-settings-page.php

我们需要一个能编辑路径和等级的界面。请用以下代码完全替换原文件内容

<?php
class PSG_Settings_Page {
    private $option_name = 'psg_protected_paths';
    private $settings_group = 'psg-settings-group';
    // 定义角色等级映射
    private $role_levels = array(
        'administrator' => 10,
        'editor'        => 7,
        'author'        => 5,
        'contributor'   => 3,
        'subscriber'    => 1,
    );
    
    public function __construct() {
        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_action('admin_init', array($this, 'register_settings'));
    }
    
    public function add_admin_menu() {
        add_options_page('PSG 角色权限禁区', 'PSG角色权限禁区', 'manage_options', 'psg-protected-paths', array($this, 'render_settings_page'));
    }
    
    public function register_settings() {
        register_setting($this->settings_group, $this->option_name, array($this, 'sanitize_paths'));
        add_settings_section('psg_main_section', '管理基于角色的访问路径', null, 'psg-protected-paths');
        add_settings_field('psg_paths_field', '路径与权限等级', array($this, 'render_paths_field'), 'psg-protected-paths', 'psg_main_section');
    }
    
    public function render_paths_field() {
        $paths = get_option($this->option_name, array());
        echo '<div id="psg-paths-container">';
        if (empty($paths)) {
            $paths = array( array('path' => '', 'min_level' => 1) );
        }
        foreach ($paths as $index => $path_entry) {
            echo '<div class="psg-path-row" style="margin-bottom:10px;">';
            echo '<input type="text" name="psg_protected_paths['.$index.'][path]" value="'.esc_attr($path_entry['path']).'" placeholder="如: /wp-content/secret/" style="width:300px;margin-right:10px;">';
            echo '<select name="psg_protected_paths['.$index.'][min_level]">';
            foreach ($this->role_levels as $role => $level) {
                $selected = selected($path_entry['min_level'], $level, false);
                echo '<option value="'.$level.'" '.$selected.'>'.ucfirst($role).' (等级 '.$level.')</option>';
            }
            echo '</select>';
            echo '<button type="button" class="button psg-remove-row" style="margin-left:10px;">移除</button>';
            echo '</div>';
        }
        echo '</div>';
        echo '<button type="button" class="button" id="psg-add-row">添加新规则</button>';
        echo '<p class="description">每条规则指定一个路径和<strong>允许访问的最低角色等级</strong>。未达到此等级的用户(包括更高等级)访问均会被拦截。</p>';
        // 内联JS,用于动态添加/删除行
        ?>
        <script type="text/javascript">
            jQuery(document).ready(function($) {
                var rowIndex = <?php echo count($paths); ?>;
                $('#psg-add-row').on('click', function() {
                    var html = '<div class="psg-path-row" style="margin-bottom:10px;">' +
                               '<input type="text" name="psg_protected_paths[' + rowIndex + '][path]" placeholder="如: /wp-content/secret/" style="width:300px;margin-right:10px;">' +
                               '<select name="psg_protected_paths[' + rowIndex + '][min_level]">' +
                               <?php foreach ($this->role_levels as $role => $level): ?>
                               '<option value="<?php echo $level; ?>"><?php echo ucfirst($role); ?> (等级 <?php echo $level; ?>)</option>' +
                               <?php endforeach; ?>
                               '</select>' +
                               '<button type="button" class="button psg-remove-row" style="margin-left:10px;">移除</button>' +
                               '</div>';
                    $('#psg-paths-container').append(html);
                    rowIndex++;
                });
                $(document).on('click', '.psg-remove-row', function() {
                    $(this).closest('.psg-path-row').remove();
                });
            });
        </script>
        <?php
    }
    
    public function sanitize_paths($input) {
        $sanitized = array();
        if (is_array($input)) {
            foreach ($input as $entry) {
                $path = sanitize_text_field(trim($entry['path'] ?? ''));
                $level = absint($entry['min_level'] ?? 1);
                if (!empty($path)) {
                    $sanitized[] = array('path' => $path, 'min_level' => $level);
                }
            }
        }
        return $sanitized;
    }
    
    public function render_settings_page() {
        ?>
        <div class="wrap">
            <h1>PSG - 基于角色的严格权限控制</h1>
            <form method="post" action="options.php">
                <?php settings_fields($this->settings_group); ?>
                <?php do_settings_sections('psg-protected-paths'); ?>
                <?php submit_button('保存所有规则'); ?>
            </form>
            <hr>
            <h3>权限等级说明</h3>
            <ul>
                <li><strong>严格层级模型</strong>:每个角色有固定等级。用户<strong>必须达到</strong>路径要求的最低等级才能访问。</li>
                <li><strong>高等级不代表万能</strong>:为“作者上传目录”设置等级5,则<strong>编辑(7)和管理员(10)访问也会被拒绝</strong>,实现“高权者不能访问低权专属区”。</li>
                <li><strong>访客(等级1)的禁区</strong>:为其设置等级>1的路径(如后台登录页<code>/wp-login.php</code>可设为5),任何越权访问将立刻被拦截。</li>
            </ul>
        </div>
        <?php
    }
    
    public static function get_protected_paths() {
        return get_option('psg_protected_paths', array());
    }
    
    public static function get_user_level($user) {
        $role_levels = array(
            'administrator' => 10,
            'editor'        => 7,
            'author'        => 5,
            'contributor'   => 3,
            'subscriber'    => 1,
        );
        $user_roles = $user->roles;
        $highest_level = 1; // 默认访客等级
        foreach ($user_roles as $role) {
            if (isset($role_levels[$role])) {
                $highest_level = max($highest_level, $role_levels[$role]);
            }
        }
        return $highest_level;
    }
}
?>

第三步:升级监控逻辑(更新 class-employee-monitor.php

监控类需要调用新的角色等级逻辑。主要修改 monitor_all_requests 方法中判断部分。

public function monitor_all_requests() {
    $request_uri = $_SERVER['REQUEST_URI'];
    $request_path = parse_url($request_uri, PHP_URL_PATH);
    $normalized_request_path = $this->normalize_path($request_path);
    
    // 获取当前用户及其等级
    $current_user = wp_get_current_user();
    $user_level = PSG_Settings_Page::get_user_level($current_user);
    psg_log("PSG_DEBUG: 用户 ‘{$current_user->user_login}‘ 等级: {$user_level}, 请求路径: {$request_path}");
    
    $protected_paths = PSG_Settings_Page::get_protected_paths();
    if (empty($protected_paths)) return;
    
    foreach ($protected_paths as $entry) {
        $protected_path = $this->normalize_path(trim($entry['path']));
        $required_level = absint($entry['min_level']);
        if (empty($protected_path)) continue;
        
        // 匹配路径
        if (strpos($normalized_request_path, $protected_path) === 0) {
            psg_log("PSG_DEBUG: 路径匹配。要求等级: {$required_level}, 用户等级: {$user_level}");
            
            // 核心:严格权限检查
            if ($user_level < $required_level) {
                // 用户等级不达标,执行拦截
                if ($this->is_malicious_direct_access()) {
                    $this->handle_violation($entry, $request_path, $user_level, $required_level);
                    return; // 拦截后退出
                }
            } else {
                // 用户等级达标,无论是否为直接访问,都允许(根据你的需求,也可在此处加入直接访问判断以更严格)
                psg_log("PSG_DEBUG: 用户等级达标,允许访问。");
            }
            break; // 匹配到一条规则后即跳出循环
        }
    }
}

同时,更新违规处理函数 handle_violation,加入等级信息:

private function handle_violation($path_entry, $request_path, $user_level, $required_level) {
    $current_user = wp_get_current_user();
    $client_ip = $_SERVER['REMOTE_ADDR'];
    psg_log("PSG_ALERT: 权限等级违规!路径: {$path_entry['path']}, 要求等级: {$required_level}, 用户等级: {$user_level}, IP: {$client_ip}");
    
    // 根据用户角色执行不同操作
    if ($current_user->exists()) {
        if (in_array('author', (array) $current_user->roles)) {
            $this->block_user($current_user, 'insufficient_level', $path_entry['path']);
        } else {
            // 其他已登录角色(如编辑)也触发严厉措施,例如强制登出并警告
            wp_logout();
            status_header(403);
            die('<h1>403 权限禁止</h1><p>您的角色无权访问此专用资源。</p>');
        }
    } else {
        // 访客触发最严厉拦截(429或立即IP黑名单)
        status_header(429);
        die('<h1>429 禁止访问</h1><p>未授权访问尝试已被记录。</p>');
    }
}

配置与测试流程

  1. 更新插件文件:替换上述三个文件(主文件、设置页、监控类)。
  2. 配置角色禁区
    • 进入 “设置” -> “PSG角色权限禁区”
    • 添加规则,例如:
      • /wp-content/uploads/author-private/ -> 作者 (等级 5):创建一个只有作者能访问的目录。
      • /wp-admin/edit.php -> 编辑 (等级 7):限制作者访问文章列表页(如果他们不该管理所有文章)。
      • /wp-login.php -> 作者 (等级 5)即使访客知道登录地址,尝试访问也会因等级不足被拒
  3. 严格测试
    • 测试1(作者访问专属目录):作者访问 /wp-content/uploads/author-private/,应成功(等级达标)。
    • 测试2(编辑访问作者专属区):编辑访问同一目录,应被拦截(等级7 > 5,但不符合“正好为5”的严格模型)。
    • 测试3(访客越界):未登录访客访问任何等级 >1 的路径(如登录页),应立刻被严厉拦截
    • 测试4(路径穿越):尝试用 /wp-content/uploads/author-private/../ 等方式绕过,因路径规范化,同样会被匹配和拦截。

「Preluna Security Guard」插件架构与逻辑说明文档

一、 核心设计理念:基于角色的最小权限等级模型

  1. 核心理念:将传统的“高权限访问低权限资源”模型,转变为 “每个资源有明确的访问门槛,任何未达标者皆被拒绝” 的严格模型。
  2. 核心映射
    • 用户 → 等级:每个用户角色(含“访客”)被赋予一个固定的权限等级数值。此等级代表其身份,而非“万能钥匙”。
    • 路径 → 等级:每条受保护的路径(文件或目录)设置一个 “最小允许访问等级”。这代表访问该资源所需的最低身份门槛
  3. 黄金规则:当用户尝试访问路径时,系统比较 用户等级 >= 路径要求等级结果为假,则无条件拒绝。这意味着:
    • 为“作者专用目录”设置等级5,则等级10的管理员和等级7的编辑访问也会被拒绝。
    • 为“后台登录页”设置等级5,则等级1的访客任何访问尝试将立即被拦截。

二、 系统架构与文件调用关系

preluna-security-guard/          # 插件根目录
├── preluna-security-guard.php   # 主控制器:定义插件、加载核心类、管理生命周期
├── includes/                    # 核心功能模块
│   ├── class-employee-monitor.php # 大脑:监听请求、进行权限判断、执行封禁
│   ├── class-settings-page.php    # 交互界面:提供后台页面以配置路径与等级
│   └── class-guest-limiter.php    # 辅助模块:独立处理访客频率限制(当前未深度集成)
└── (未来的) uninstall.php       # 卸载清理脚本

文件调用流程

  1. 初始化:WordPress加载插件,执行 preluna-security-guard.php 中的 psg_initialize() 函数。
  2. 加载模块:该函数实例化三个核心类 PSG_Employee_Monitor, PSG_Guest_Limiter, PSG_Settings_Page。它们通过WordPress的钩子系统嵌入相应流程。
  3. 分工协作
    • 设置页面类:响应管理员在后台的菜单点击,渲染并处理表单,将配置保存至数据库选项 psg_protected_paths
    • 监控核心类:在用户每个页面请求的早期(init 和 admin_init 钩子),从数据库读取配置,执行权限判断。
    • 频率限制类:在每次请求时(init 钩子),独立检查未登录用户的访问频率。

三、 核心功能实现逻辑详解

1. 权限判断与拦截流程 自己被自己写的东西给限制死了。:(

  • 关键函数
    • normalize_path($path): 防御路径穿越攻击的核心。通过解析和重组路径部件,将类似 /safe/../secret/file.txt 的请求规范化为 /secret/file.txt,确保后续匹配准确。
    • is_malicious_direct_access(): 智能放行的关键。通过检查HTTP请求头中的 Referer(来源页)是否来自本站,以及 User-Agent 是否包含已知扫描器特征,来区分“恶意直接访问”和“网站正常功能调用”。
    • PSG_Settings_Page::get_user_level($user): 计算用户等级的核心。根据预设的角色等级映射表,返回用户所有角色中的最高等级。

2. 封禁处罚机制
处罚的严厉程度根据入侵者身份呈阶梯式上升

触发者身份处罚机制 (handle_violation & block_user)设计意图
作者1. 清空账号所有角色 ($user->set_role(''))。
2. 强制登出当前所有会话。
3. 发送邮件通知管理员。
4. 重定向至警告页。
最严厉的应用层封禁。账号即刻失效,且无法自行恢复,需管理员干预。旨在内部震慑。
其他登录用户 (如编辑)1. 强制登出当前会话。
2. 返回 403 Forbidden 错误页。
严厉警告。剥夺其当前会话,明确告知越权,但不直接销毁账号。
未登录访客返回 429 Too Many Requests 或自定义混淆信息。网络层拦截。消耗攻击者资源,同时避免暴露网站结构。可无缝衔接未来IP黑名单。

四、 已识别的问题与后续改进思路

基于测试人员提出的三点,以下是分析和修改思路,我们会在后续进行完善。

1. 防傻瓜操作:增强路径输入验证

  • 问题:当前仅做了基础的 trim() 和 sanitize_text_field() 处理,用户可能输入无效、重复或格式混乱的路径。
  • 改进思路
    • 前端即时验证:在设置页面添加JavaScript,在用户输入时实时检查路径格式(如是否以 / 开头,是否包含非法字符如 :*?<>|)。
    • 后端强化清洗:在 sanitize_paths() 函数中,增加逻辑:去除重复条目、自动为目录路径补全末尾的 /(如果意图是保护目录)、验证路径是否确实存在于网站目录结构中(可选,但能极大防错)。
    • 提供示例与模板:在输入框旁提供更醒目的格式示例和“常用路径”模板按钮。

2. 路径预览与效果可视化

  • 问题:配置是抽象的文本列表,管理员无法直观感受“哪些真实文件将被保护”。
  • 改进思路
    • 创建“路径检测器”工具:在设置页面新增一个独立区域,允许管理员输入一个测试URL,插件即时返回匹配结果、所需等级、当前用户等级及模拟访问结果。
    • 生成静态报告:添加一个“生成报告”按钮,脚本扫描 wp-content 等目录,列出所有文件,并对照当前规则标记出哪些会被保护及其等级,形成一个HTML报告。

3. 实现“白名单+黑名单”混合模型

  • 问题:当前是单一的“门槛”模型。有时需要更灵活:例如“允许管理员访问某个作者目录”(白名单),或“禁止所有人(包括管理员)访问某个日志文件”(黑名单)。
  • 改进思路 – 数据结构改造
// 新的配置数据结构示例
$rule = array(
    'path' => '/wp-content/debug.log',
    'type' => 'blacklist', // 或 ‘whitelist‘, ‘min_level‘
    'value' => true, // 对于blacklist, true即完全拒绝
    // 或 ‘value‘ => 10, // 对于min_level, 表示所需等级
    // 或 ‘value‘ => array('administrator', 'editor'), // 对于whitelist, 指定允许的角色数组
);
  • min_level:保持现有严格等级模型。
  • whitelist:优先级最高。指定允许的角色数组,仅列表内角色可访问,其他一律拒绝(即使等级更高)。
  • blacklist:优先级次高。无论用户身份如何,一律拒绝访问。用于保护极度敏感文件。
  • 判断逻辑更新:监控类的判断流程需调整,按 白名单 → 黑名单 → 最小等级 的顺序进行检查,后者作为默认规则。

五、 权限等级说明详细化建议

在设置页面的说明部分,可以增加以下内容,使其更具指导性:

权限等级配置实战指南

  • 等级1 (访客/订阅者):用于完全公开的资源。任何高于此等级的设置,都将导致访客被拦截。可将/wp-login.php设为5,以隐藏登录入口。
  • 等级5 (作者):作者专属空间。例如 /wp-content/uploads/personal/,设置后,编辑和管理员也将无法访问,实现了作者隐私。
  • 等级7 (编辑):编辑部资源。例如共享的稿件库 /wp-content/uploads/draft-pool/,设置后,作者无法查看,但编辑和管理员可以。
  • 等级10 (管理员):核心系统文件。例如 /wp-config.php、数据库备份目录。建议不要设置为10,而应使用未来的“黑名单”功能直接禁止所有访问,因为即使是管理员,日常也无须直接访问这些文件。

核心原则:为路径设置的等级,应等于有合理、日常业务需求访问该资源的最低级别角色

防傻瓜操作:增强路径输入验证

这个功能的目标是确保管理员在后台填写的每一条“保护路径”都是格式正确、无重复、且安全有效的,从源头上杜绝配置错误。我们将从 前端即时提示 和 后端严格清洗 两个方面对设置页面进行加固。

第一步:修改设置页面类 (includes/class-settings-page.php)

我们将主要修改两个核心方法:render_paths_field(用于前端渲染和交互)和 sanitize_paths(用于后端清洗和验证)。

1. 增强前端输入与即时验证

在 render_paths_field 方法中,我们需要在输出HTML和JavaScript时,增加对路径格式的即时检查。

主要改进点

  • 输入提示:在输入框内增加更明确的 placeholder 说明。
  • 实时格式检查:通过JavaScript,在用户输入时或失去焦点时,检查路径格式,并用醒目的颜色(如红色)提示错误(例如路径不以 / 开头,或包含非法字符)。
  • 重复项高亮:在客户端检查同一页面内是否有重复的路径输入,并提示用户。

由于代码较长,关键是在输出HTML部分后,加入相应的JavaScript。例如,可以在原有的内联JS后,增加格式验证函数:

// ... 原有的动态添加/删除行JS代码 ...

// 路径格式验证函数
function psgValidateSinglePath(inputElement) {
    var path = inputElement.value.trim();
    var feedbackSpan = inputElement.nextElementSibling;
    if (!feedbackSpan || !feedbackSpan.classList.contains('psg-path-feedback')) {
        feedbackSpan = document.createElement('span');
        feedbackSpan.className = 'psg-path-feedback';
        inputElement.parentNode.insertBefore(feedbackSpan, inputElement.nextSibling);
    }

    if (path === '') {
        feedbackSpan.textContent = '(路径为空,将被忽略)';
        feedbackSpan.style.color = '#999';
        return false;
    }
    if (path.charAt(0) !== '/') {
        feedbackSpan.textContent = '⚠️ 路径应以斜杠 (/) 开头';
        feedbackSpan.style.color = '#d63638';
        return false;
    }
    // 检查非法字符 (根据系统而定,这里是一些常见危险字符)
    var illegalChars = /[<>:"|?*\\]/;
    if (illegalChars.test(path)) {
        feedbackSpan.textContent = '⚠️ 包含非法字符';
        feedbackSpan.style.color = '#d63638';
        return false;
    }
    // 简单的目录路径自动补全提示(非强制)
    if (path.charAt(path.length - 1) !== '/' && path.indexOf('.') === -1) {
        // 看起来像目录但没有以/结尾,给出提示
        feedbackSpan.textContent = '💡 提示:若为目录,建议以 / 结尾';
        feedbackSpan.style.color = '#f0ad4e';
    } else {
        feedbackSpan.textContent = '✓ 格式正确';
        feedbackSpan.style.color = '#46b450';
    }
    return true;
}

// 为所有现有和未来新增的输入框绑定事件
jQuery(document).on('blur', 'input[name*="[path]"]', function() {
    psgValidateSinglePath(this);
});
// 页面加载时也验证一次
jQuery(document).ready(function($) {
    $('input[name*="[path]"]').each(function() { psgValidateSinglePath(this); });
});

第二步:测试清单

修改完成后,请按照以下步骤测试增强的验证功能:

测试场景操作预期结果
1. 前端格式验证在设置页面,输入一个不以 / 开头的路径(如 wp-admin/)。输入框旁应立即或失去焦点后出现红色警告提示。
2. 前端重复提示添加两条完全相同的路径并保存。页面提交后,后端应只保存一条。前端最好能有重复提示(我们当前的JS示例未实现重复检查,但后端已处理)。
3. 后端路径清洗输入 /wp-content/uploads//secret/(多斜杠)并保存。保存后,在数据库或重新加载页面时,应显示为规范化的 /wp-content/uploads/secret/
4. 后端重复项合并分别输入 /secret/ 和 /secret(一个带斜杠一个不带)。保存后,应只保留其中一条(取决于 normalize_input_path 的处理结果)。
5. 非法路径过滤输入一个系统文件路径如 /etc/passwd 或包含 ../ 试图穿越的路径。保存后,该条目不应出现在规则列表中(被 is_path_inside_wp_content 过滤)。
6. 空值处理添加一行,只选择等级但不填路径,然后保存。空路径的条目应被忽略,不保存。

路径预览与效果可视化

目标是让管理员在配置时,能直观、即时地看到每条规则的实际效果,将抽象的文本配置转化为清晰的可视化反馈。

  1. 实时路径测试器:输入任意路径,立即看到匹配结果和访问状态。
  2. 规则影响范围预览(可选):生成一个简单的报告,展示规则保护了哪些典型路径。

工具将嵌入到设置页面中,让管理员随时验证配置。

第一步:在设置页面类中添加预览界面

在 includes/class-settings-page.php 的 render_settings_page() 方法中,在表单结束标签 </form> 之后、说明部分之前,添加以下HTML和逻辑代码:

// ... 原有的表单和提交按钮代码 ...
</form>

<hr>

<!-- 新增:路径测试与预览区域 -->
<div class="wrap" style="margin-top: 30px; background: #f6f7f7; padding: 20px; border: 1px solid #c3c4c7;">
    <h2>🔍 路径测试与规则预览</h2>
    <p>在此测试任意路径,查看它将如何被当前规则匹配和处理。</p>

    <table class="form-table">
        <tr>
            <th scope="row"><label for="psg_test_path">输入测试路径</label></th>
            <td>
                <input type="text" id="psg_test_path" name="psg_test_path" value="" class="regular-text" placeholder="例如:/wp-content/uploads/secret/file.jpg" style="width: 300px;">
                <button type="button" id="psg_test_button" class="button button-secondary">立即测试</button>
                <p class="description">输入一个完整的网站路径(以 / 开头)进行测试。</p>
            </td>
        </tr>
        <tr>
            <th scope="row">测试结果</th>
            <td>
                <div id="psg_test_result" style="padding: 15px; border: 1px dashed #ccc; min-height: 60px; background: white;">
                    <p style="color: #999; margin: 0;">点击“立即测试”按钮后,结果将显示在这里。</p>
                </div>
            </td>
        </tr>
    </table>

    <h3>📋 当前所有规则预览</h3>
    <p>下方表格列出了所有已生效的保护规则。你可以在此复核。</p>
    <?php
    $current_rules = self::get_protected_paths();
    if (empty($current_rules)) {
        echo '<p><strong>暂无生效的规则。</strong></p>';
    } else {
        echo '<table class="wp-list-table widefat striped" style="width: auto;">';
        echo '<thead><tr><th>路径 (Path)</th><th>要求最低角色等级</th><th>匹配示例</th></tr></thead>';
        echo '<tbody>';
        $role_names = array(1=>'访客/订阅者', 3=>'投稿者', 5=>'作者', 7=>'编辑', 10=>'管理员');
        foreach ($current_rules as $rule) {
            $example = $rule['path'];
            // 如果路径是目录(以/结尾),为其生成一个示例文件
            if (substr($rule['path'], -1) === '/') {
                $example = $rule['path'] . 'example-file.txt';
            }
            echo '<tr>';
            echo '<td><code>' . esc_html($rule['path']) . '</code></td>';
            echo '<td><strong>' . $role_names[$rule['min_level']] . '</strong> (等级 ' . $rule['min_level'] . ')</td>';
            echo '<td><code>' . esc_html($example) . '</code></td>';
            echo '</tr>';
        }
        echo '</tbody></table>';
    }
    ?>
</div>

<!-- 内联JavaScript,处理测试按钮的异步请求 -->
<script type="text/javascript">
jQuery(document).ready(function($) {
    $('#psg_test_button').on('click', function() {
        var testPath = $('#psg_test_path').val().trim();
        if (!testPath) {
            alert('请输入要测试的路径。');
            return;
        }
        // 禁用按钮,显示加载中
        var $button = $(this);
        $button.prop('disabled', true).text('测试中...');
        $('#psg_test_result').html('<p style="color: #999;"><span class="spinner is-active" style="float:none;"></span> 正在分析路径并匹配规则...</p>');

        // 发起AJAX请求到WordPress后端
        $.ajax({
            url: ajaxurl, // WordPress 定义的全局变量,指向 admin-ajax.php
            type: 'POST',
            data: {
                action: 'psg_preview_path', // 我们稍后要注册的AJAX动作
                path: testPath,
                _wpnonce: '<?php echo wp_create_nonce('psg_preview_nonce'); ?>' // 安全验证
            },
            success: function(response) {
                $button.prop('disabled', false).text('立即测试');
                if (response.success) {
                    $('#psg_test_result').html(response.data);
                } else {
                    $('#psg_test_result').html('<p style="color: #d63638;">❌ 请求失败:' + response.data + '</p>');
                }
            },
            error: function() {
                $button.prop('disabled', false).text('立即测试');
                $('#psg_test_result').html('<p style="color: #d63638;">❌ 网络请求失败,请检查控制台或稍后重试。</p>');
            }
        });
    });
});
</script>

<!-- 以下是原有的“权限等级说明”部分 -->
<hr>
<h3>权限等级说明</h3>

第二步:注册AJAX处理函数,实现路径分析逻辑

我们需要让前端JavaScript能调用后端PHP函数来分析路径。在 class-settings-page.php 的 __construct 构造函数中,添加AJAX钩子注册:

public function __construct() {
    add_action('admin_menu', array($this, 'add_admin_menu'));
    add_action('admin_init', array($this, 'register_settings'));
    // +++ 新增:注册AJAX处理函数 +++
    add_action('wp_ajax_psg_preview_path', array($this, 'ajax_preview_path'));
}

然后,在类中添加新的 ajax_preview_path 方法:

/**
 * AJAX处理函数:分析给定路径,并返回匹配结果和访问状态。
 */
public function ajax_preview_path() {
    // 1. 安全验证
    check_ajax_referer('psg_preview_nonce', '_wpnonce');
    if (!current_user_can('manage_options')) {
        wp_die('权限不足。');
    }

    // 2. 获取并清理测试路径
    $test_path = isset($_POST['path']) ? sanitize_text_field($_POST['path']) : '';
    if (empty($test_path) || strpos($test_path, '/') !== 0) {
        wp_send_json_error('请输入一个以斜杠 (/) 开头的有效路径。');
    }

    // 3. 获取当前用户和所有规则
    $current_user = wp_get_current_user();
    $user_level = self::get_user_level($current_user);
    $all_rules = self::get_protected_paths();

    // 4. 调用一个方法来分析路径 (我们将创建这个方法)
    $analysis_result = $this->analyze_path_for_preview($test_path, $user_level, $all_rules);

    // 5. 返回格式化的HTML结果
    wp_send_json_success($analysis_result);
}

/**
 * 分析路径的核心逻辑
 */
private function analyze_path_for_preview($input_path, $user_level, $all_rules) {
    $output = '<div class="psg-preview-result">';
    
    // a. 显示基本信息
    $normalized_path = $this->normalize_input_path($input_path);
    $output .= '<p><strong>测试路径:</strong><code>' . esc_html($input_path) . '</code></p>';
    if ($input_path !== $normalized_path) {
        $output .= '<p><strong>规范化后:</strong><code>' . esc_html($normalized_path) . '</code></p>';
    }
    $output .= '<p><strong>当前用户:</strong>' . esc_html($current_user->user_login) . ' (权限等级:<strong>' . $user_level . '</strong>)</p>';
    $output .= '<hr style="margin: 15px 0;">';

    // b. 开始匹配规则
    $matched_rule = null;
    foreach ($all_rules as $rule) {
        $rule_path = $this->normalize_input_path($rule['path']);
        // 检查测试路径是否以规则路径开头
        if (strpos($normalized_path, $rule_path) === 0) {
            $matched_rule = $rule;
            break; // 找到第一条匹配的规则就停止
        }
    }

    if ($matched_rule) {
        $output .= '<p><span style="color:#46b450;">✅ <strong>匹配到规则!</strong></span></p>';
        $output .= '<ul>';
        $output .= '<li><strong>规则路径:</strong><code>' . esc_html($matched_rule['path']) . '</code></li>';
        $output .= '<li><strong>要求最低等级:</strong><span style="font-weight:bold;">' . $matched_rule['min_level'] . '</span></li>';
        $output .= '</ul>';

        // c. 模拟访问决策
        $role_names = array(1=>'访客/订阅者', 3=>'投稿者', 5=>'作者', 7=>'编辑', 10=>'管理员');
        $required_role_name = $role_names[$matched_rule['min_level']] ?? '等级' . $matched_rule['min_level'];
        $user_role_name = $role_names[$user_level] ?? '等级' . $user_level;

        if ($user_level < $matched_rule['min_level']) {
            $output .= '<div style="padding: 15px; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;">';
            $output .= '<p><strong>🚫 访问将被拦截!</strong></p>';
            $output .= '<p>您的等级 (<strong>' . $user_role_name . '</strong>) 低于此资源要求的等级 (<strong>' . $required_role_name . '</strong>)。</p>';
            $output .= '<p><em>根据插件设置,这将触发相应的封禁或拦截措施。</em></p>';
            $output .= '</div>';
        } else {
            $output .= '<div style="padding: 15px; background-color: #d1ecf1; border: 1px solid #bee5eb; border-radius: 4px; color: #0c5460;">';
            $output .= '<p><strong>✅ 访问将被允许。</strong></p>';
            $output .= '<p>您的等级 (<strong>' . $user_role_name . '</strong>) 已达到或超过要求等级 (<strong>' . $required_role_name . '</strong>)。</p>';
            $output .= '</div>';
        }
    } else {
        $output .= '<p><span style="color:#6c757d;">ℹ️ <strong>未匹配任何规则。</strong></span></p>';
        $output .= '<p>此路径不在当前的保护规则列表中,按照默认策略,访问将被允许。</p>';
    }

    $output .= '</div>';
    return $output;
}

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇