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

上图展示了从低到高的角色权限。对于构建的“前端投稿-后台审核”系统,投稿者 (Contributor) 和订阅者 (Subscriber) 是最相关的两个角色。
权限隔离
暴露了服务器文件路径是一个严重的安全隐患。这通常意味着上传的文件被直接链接到了服务器的绝对路径(例如 http://你的网站.com/wp-content/uploads/...),这会让技术型用户窥探到你网站的部分目录结构。
问题的根源在于 “对上传文件的访问控制不够严格” 。我们需要建立两层防护:
- 第一层:隐藏路径 – 确保前端不直接显示服务器物理路径。
- 第二层:访问拦截 – 即使有人猜到了文件路径,也无法直接通过浏览器访问,必须经过网站程序(和权限检查)。
加固步骤一:配置Flamingo插件(隐藏路径)
我们的目标是让Flamingo后台只显示用于管理的文件链接,而不暴露具体路径。
- 进入Flamingo设置:在WordPress后台,进入 Flamingo → 设置。
- 检查“文件上传”设置:找到与“文件上传”或“附件链接”相关的选项。
- 修改链接类型(关键):如果存在相关选项,尝试将文件链接的显示方式从 “绝对URL” 或 “文件路径” 改为 “相对URL” 或仅显示文件名。不同版本的插件设置可能不同,如果找不到,这一步可以暂时跳过,我们通过第二步从根本上解决。
加固步骤二:保护上传目录(根本性解决)
这是最有效的一步,通过服务器规则禁止直接访问存放投稿文件的目录,只允许WordPress程序本身(经过权限验证后)读取文件。
- 找到上传目录路径:使用FTP工具或主机商的文件管理器,进入你网站的
wp-content/uploads/目录。 - 创建保护文件:在该目录下,检查并创建一个名为
.htaccess的文件(如果已存在,则编辑它)。 - 添加安全规则:在
.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/
这是如何运作的?
- 临时存储:用户提交表单时,文件首先会传到这个目录,文件名会被系统自动重命名为一串随机字符(如
a1b2c3d4e5.pdf),这本身就隐藏了原始文件名。 - 记录链接:Flamingo 插件在记录这次提交时,会在数据库里保存一个指向这个临时文件的链接。
- 自动清理:默认情况下,这些临时上传的文件会在24小时后被系统自动删除。这就是为什么必须安装Flamingo来永久保存记录的原因,它保存的是“文件曾经在此”的信息,而不是文件本身。
重要提示:如果想让文件永久保存,需要安装额外的扩展插件(如 Contact Form 7 File Download),它会将文件转移到媒体库。但目前你的免费方案中,Flamingo + 邮件通知已足够你获取文件。
如何在Flamingo中管理文件?(防止暴露)
Flamingo本身没有复杂的“文件上传”设置页面。它的核心是记录数据。你真正需要做的安全加固,是在服务器层面阻止对这个上传目录的直接访问。
操作路径如下:
- 找到目录:使用你的主机控制面板(如cPanel)的“文件管理器”,或FTP工具(如FileZilla),连接到你的网站。
- 导航到路径:依次打开
wp-content→uploads文件夹。 - 确认子目录:查看里面是否有
wpcf7_uploads文件夹。这就是CF7上传文件的确切位置。 - 实施保护(核心步骤):在该
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)
当前首要任务是切断所有对上传目录的直接访问。你的规则需要简化,避免内部冲突。
- 定位文件:使用主机商的文件管理器或FTP工具,进入
/wp-content/uploads/wpcf7_uploads/目录。 - 创建/编辑文件:创建或清空里面的
.htaccess文件。 - 写入最严格规则:只保留下面三行(适用于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文件。
- 登录WordPress后台,进入 外观 -> 主题文件编辑器。
- 在右侧找到
functions.php,点击进行编辑。 - 在文件末尾的
?>标签之前(如果没有?>,就加在文件最后),添加以下代码:
/**
* 防御加固第一步:将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...)$"> 匹配末尾的 .php,1.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 中添加代码,实现:
- 立即转移并重命名:上传后,文件立刻被移出Web根目录(如
/home/user/private_)并重命名为随机字符串(如a1b2c3d4e5)。攻击者即使上传成功,也永远不知道文件在哪、叫什么。 - 验证文件“魔数”:用PHP的
finfo_file()函数读取文件头部字节签名(如FF D8 FF E0代表JPEG),判断真实类型,与后缀名严格比对。这可以防御所有文件名/编码欺骗。 - 图片文件“再编码”:对允许的图片,用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) 必须保留。这是最后一道物理屏障,即使有文件因未知漏洞被放到了这里,也无法直接访问。
第四层:运维层面防护
- 保持所有软件更新:WordPress核心、插件、主题、PHP版本、服务器系统。
- 使用专业安全插件:如 Wordfence,它具备防火墙和文件完整性监控功能,能主动阻断可疑请求,并在文件被篡改时报警。
- 最小化暴露:服务器错误页面不显示路径信息。
Nginx版本
什么你告诉我你的配置已经修改完了,但是测试的时候却没有返回效果…..
这是一个非常关键的问题,说明安全屏障没有生效。403未出现,通常意味着 .htaccess 文件未被Apache服务器正确读取或执行。

第一步:最可能的原因——文件放错了位置
.htaccess 的规则只对它所在目录及其子目录生效。你必须将文件放在你想要保护的那个具体文件夹里。
- 请再次确认:你修改的是哪个
.htaccess文件?你需要保护的是wpcf7_uploads目录。 - 正确路径:包含
Require all denied规则的.htaccess文件,必须位于以下路径:/wp-content/uploads/wpcf7_uploads/.htaccess
(请注意文件名以点开头) - 检查方法:用FTP或文件管理器,导航到这个目录,确认
.htaccess文件就在这里,而不是在网站根目录或其他地方。
第二步:检查规则本身与服务器环境
- 规则独立性:确保你的规则没有错误地嵌套在
<IfModule mod_rewrite.c>或# BEGIN WordPress等任何其他模块区块内。在wpcf7_uploads目录下的.htaccess,内容应该只有纯粹的安全规则,例如:apache# 无条件拒绝所有访问 Require all denied Options -Indexes不要包含RewriteEngine On等WordPress重写规则。 - 测试
.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; # 让整个网站对无效路径都返回混淆信息
}
log_not_found off:这个指令非常有用,它可以阻止Nginx将“文件未找到”的错误记录到error.log中。这意味着攻击者即使反复探测,也不会在服务器日志中留下清晰的“404”痕迹,进一步实现了模糊化。- 作用范围:以上配置主要针对
wpcf7_uploads目录。如果你将最下方的location /块也修改了,那么网站所有不存在的路径(比如攻击者胡乱猜测的/admin.php、/wp-login.php)都会返回同样的混淆信息,实现全局深度防御。 - 内容设计:
return 200后面的内容可以精心设计,例如模仿一些常见的服务端错误信息、JSON格式的乱码,或者直接是一个空白页。
如果有异常的行为,对IP进行封禁
要实现动态IP封禁和错误混淆,需要结合Nginx配置和外部工具/脚本。
一、核心配置要点澄清
需要理解并操作两类文件,它们共同构成防御体系
二、实现方案与配置方法
下面是一个结合了所有需求(混淆、动态封禁、豁免测试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:
- 封禁IP:
sudo manage_blockip.sh add 攻击者IP - 解封IP:
sudo 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)就像是你的 “房间号” 。这个号码只在公寓楼内部使用,邮递员无法直接把信送到这个“房间号”。- 百度搜索出来的IP(
223.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.x或100.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 );
}
}
}
?>
第四步:安装、调试与测试
- 安装插件:将整个
preluna-security-guard文件夹通过FTP或文件管理器上传到wp-content/plugins/。 - 激活插件:在WordPress后台的“插件”页面,找到 “Preluna Security Guard” 并激活它。
- 调试模式:在调试时,强烈建议在你的
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();
}
使用与测试指南
- 更新插件:将上述三个新文件上传并修改主文件后,在后台重新启用插件。
- 配置禁区:进入 “设置” -> “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>');
}
}
配置与测试流程
- 更新插件文件:替换上述三个文件(主文件、设置页、监控类)。
- 配置角色禁区:
- 进入 “设置” -> “PSG角色权限禁区”。
- 添加规则,例如:
/wp-content/uploads/author-private/-> 作者 (等级 5):创建一个只有作者能访问的目录。/wp-admin/edit.php-> 编辑 (等级 7):限制作者访问文章列表页(如果他们不该管理所有文章)。/wp-login.php-> 作者 (等级 5):即使访客知道登录地址,尝试访问也会因等级不足被拒。
- 严格测试:
- 测试1(作者访问专属目录):作者访问
/wp-content/uploads/author-private/,应成功(等级达标)。 - 测试2(编辑访问作者专属区):编辑访问同一目录,应被拦截(等级7 > 5,但不符合“正好为5”的严格模型)。
- 测试3(访客越界):未登录访客访问任何等级 >1 的路径(如登录页),应立刻被严厉拦截。
- 测试4(路径穿越):尝试用
/wp-content/uploads/author-private/../等方式绕过,因路径规范化,同样会被匹配和拦截。
- 测试1(作者访问专属目录):作者访问
「Preluna Security Guard」插件架构与逻辑说明文档
一、 核心设计理念:基于角色的最小权限等级模型
- 核心理念:将传统的“高权限访问低权限资源”模型,转变为 “每个资源有明确的访问门槛,任何未达标者皆被拒绝” 的严格模型。
- 核心映射:
- 用户 → 等级:每个用户角色(含“访客”)被赋予一个固定的权限等级数值。此等级代表其身份,而非“万能钥匙”。
- 路径 → 等级:每条受保护的路径(文件或目录)设置一个 “最小允许访问等级”。这代表访问该资源所需的最低身份门槛。
- 黄金规则:当用户尝试访问路径时,系统比较
用户等级 >= 路径要求等级。结果为假,则无条件拒绝。这意味着:- 为“作者专用目录”设置等级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 # 卸载清理脚本
文件调用流程:
- 初始化:WordPress加载插件,执行
preluna-security-guard.php中的psg_initialize()函数。 - 加载模块:该函数实例化三个核心类
PSG_Employee_Monitor,PSG_Guest_Limiter,PSG_Settings_Page。它们通过WordPress的钩子系统嵌入相应流程。 - 分工协作:
设置页面类:响应管理员在后台的菜单点击,渲染并处理表单,将配置保存至数据库选项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()函数中,增加逻辑:去除重复条目、自动为目录路径补全末尾的/(如果意图是保护目录)、验证路径是否确实存在于网站目录结构中(可选,但能极大防错)。 - 提供示例与模板:在输入框旁提供更醒目的格式示例和“常用路径”模板按钮。
- 前端即时验证:在设置页面添加JavaScript,在用户输入时实时检查路径格式(如是否以
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. 空值处理 | 添加一行,只选择等级但不填路径,然后保存。 | 空路径的条目应被忽略,不保存。 |
路径预览与效果可视化
目标是让管理员在配置时,能直观、即时地看到每条规则的实际效果,将抽象的文本配置转化为清晰的可视化反馈。
- 实时路径测试器:输入任意路径,立即看到匹配结果和访问状态。
- 规则影响范围预览(可选):生成一个简单的报告,展示规则保护了哪些典型路径。
工具将嵌入到设置页面中,让管理员随时验证配置。
第一步:在设置页面类中添加预览界面
在 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;
}



