封禁系统与 IP 黑名单使用指南
面向站长、管理员、版主与插件开发者。技术实现细节请参阅 XiunoX 用户封禁系统 。
一、功能介绍
1.1 用户封禁
针对恶意用户(小黑子)的 4 档状态管理:
状态 | 中文 | 能否登录 | 能否浏览 | 能否发帖/回帖 | 能否改密/找密 |
|---|---|---|---|---|---|
0 | 正常 | ✅ | ✅ | ✅ | ✅ |
1 | 禁言 | ✅ | ✅ | ❌ | ✅ |
2 | 禁止访问 | ❌ | ❌ | ❌ | ✅ |
3 | 锁定 | ❌ | ❌ | ❌ | ❌ |
核心特性:
到期自动解封:访问时自动检查
banned_until,过期会重置封禁字段(不需要 cron 定时任务)永久封禁:时长选「永久」时
banned_until = 9999999999(约 2286 年),永不过期场景隔离:禁言用户仍可浏览和登录,仅在发帖/回帖入口被拦截
内容隐藏:被封用户发布的帖子/回帖对普通用户隐藏,显示占位提示「该用户被关禁闭,内容被隐藏」;管理员和作者本人可见原始内容;解封后内容自动恢复
完整历史:每次封禁/解封/自动解封/清空内容都写入
bbs_user_ban_log,可在后台查看管理员豁免:gid=1,2 的管理员组不可被封禁,管理员自己也不能被封自己
1.2 IP 黑名单 / 白名单
针对恶意 IP 的访问控制:
支持格式:
单 IP:
192.168.1.100CIDR 网段:
192.168.1.0/24IP 范围:
192.168.1.1-192.168.1.10IPv4 / IPv6 均支持
白名单优先:IP 同时在黑白名单时,白名单生效,允许访问
过期时间:可设永久或指定过期时间戳,过期后自动失效(不主动清理,查询时跳过)
覆盖入口:登录、注册、发主题三个入口会做 IP 检查
存储方式:使用
kv_set序列化存储,不依赖额外数据表(注:旧的bbs_banned_ip表已废弃,仅作历史数据兼容)
1.3 封禁公示页
路由:
/banned三栏布局(与首页一致)+ nav-tabs 切换
当前封禁中 tab:显示
ban_type > 0的用户列表,按封禁时间倒序,最多 50 条近期释放 tab:显示近 30 天解封的用户,最多 20 条
URL 支持 path 同步:
/banned//banned-current//banned-recent受
ban_show_public_list配置控制,关闭时返回 404默认开启(注:Xiuno conf 加载机制详见 user-ban-system.md)
1.4 内容清空
针对需要保留账号但清空所有内容的场景:
删除所有回帖
删除主题索引(
mythread表,不删 thread 表主题记录)删除附件
删除通知
重置
threads/posts计数为 0不删账号,用户主页显示「该用户已被管理员清空内容」提示
二、管理员使用教程
2.1 部署前的升级
部署后必须到 /admin/?upgrade.htm 执行「用户封禁系统」升级项,会自动:
为
bbs_user表添加 4 个字段(ban_type/ban_reason/ban_admin_uid/ban_time)创建
bbs_user_ban_log表(封禁历史)为已存在用户初始化
ban_type=0
不执行升级会导致封禁功能不可用,后台会报「Unknown column 'ban_type'」错误。
2.2 后台入口
后台侧栏「其他 → 安全设置」下,包含 8 个子 tab:
Tab | URL | 用途 |
|---|---|---|
发帖限制 |
| 发帖/回帖间隔、字数限制 |
账号安全 |
| 密码强度、注册间隔 |
内容权限 |
| 编辑/删除权限 |
其他设置 |
| 软删除、签名修改限制 |
验证码 |
| 验证码配置 |
敏感词 |
| 敏感词过滤 |
IP 黑名单 |
| IP 黑白名单管理 |
封禁用户 |
| 当前封禁用户列表/解封 |
2.3 封禁用户流程
方式 1:从用户列表进入
后台「用户管理 → 用户列表」
找到目标用户,点击「编辑」
在「封禁设置」区域:
选择封禁类型(禁言 / 禁止访问 / 锁定)
选择时长(1天 / 3天 / 7天 / 30天 / 永久 / 自定义)
填写封禁原因(必填,会显示给用户)
提交即可
方式 2:从封禁用户 tab 进入(推荐批量管理)
后台「安全设置 → 封禁用户」
顶部搜索框支持:
用户名模糊搜索:输入部分用户名,LIKE 匹配
UID 精确搜索:输入纯数字按 UID 精确匹配
列表中可看到封禁类型、状态、剩余时间、原因
点击「解封」按钮即可解封(会询问确认,调用
UserBanService::unban())
方式 3:从帖子页直接封禁(管理员/版主)
帖子详情页,作者卡片位置有「封禁」按钮
点击弹出封禁 Modal
选择类型和时长后提交
方式 4:举报联动
后台「举报管理」处理举报时
选择「封禁用户」会自动禁言 7 天
选择「删除并封禁」会先删内容再封禁
2.4 IP 黑名单使用
添加 IP
后台「安全设置 → IP 黑名单」
选择类型:单 IP / CIDR / 范围
填入 IP(如
192.168.1.100或192.168.1.0/24或192.168.1.1-192.168.1.10)填写备注(可选)
选择过期时间(永久或指定日期)
提交即可
IP 命中后的行为
命中黑名单的 IP 在以下入口被拦截:
登录(
route/user.phplogin)注册(
route/user.phpcreate)发主题(
route/thread.phpcreate)
命中时返回提示「您的 IP 已被加入黑名单」并中止请求
命中白名单的 IP 即使在黑名单中也允许访问(白名单优先)
2.5 版主权限限制
版主(gid=3,4,5)封禁时有严格的三重校验:
类型限制:只能禁言(
ban_type=1),不能禁止访问或锁定不可永久:
duration不能为 0时长限制:必须在 1-7 天之间(86400 ~ 604800 秒)
管理员(gid=1,2)无上述限制,可永久封禁和锁定。
2.6 公示页控制
配置项:
ban_show_public_list位置:
conf/conf.php值:
1开启(默认)/0关闭关闭后访问
/banned返回 404
注意:Xiuno 只加载
conf/conf.php,不合并conf.default.php。旧站点若conf.php没有此项,代码用isset() && empty()双重判断,默认视为开启。
三、插件开发者指南
3.1 调用前必做的依赖加载
生产环境(DEBUG=0)走 tmp/model.min.php 合并加载,类加载顺序不可预测。开发环境(DEBUG=1)因其他插件先执行而「碰巧能用」,但生产环境会抛 Class not found 致命错误。
正确写法(所有调用 UserBanService / IpBlacklistService 的插件代码):
// 调用 UserBanService 前
if(!class_exists('UserBanService')) {
include_once APP_PATH.'lib/UserBanService.php';
}
// 调用 IpBlacklistService 前
if(!class_exists('IpBlacklistService')) {
include_once APP_PATH.'lib/security/IpBlacklistService.php';
}
错误写法(生产环境会崩):
// ❌ 直接访问静态成员
if(UserBanService::ADMIN_GIDS) { ... } // 若类未加载,这里就崩溃
// ❌ 假设类已被其他代码加载
UserBanService::ban($uid, 1, 86400, 'spam', $adminUid);
3.2 在插件中封禁用户
<?php
!defined('DEBUG') AND exit('Access Denied');
// 1. 加载依赖
if(!class_exists('UserBanService')) {
include_once APP_PATH.'lib/UserBanService.php';
}
// 2. 检查 CSRF
CsrfService::check();
// 3. 接收参数(注意:reason 等文本参数第 3 个参数传 FALSE 关闭 htmlspecialchars)
$target_uid = intval(param('uid'));
$reason = param('reason', '', FALSE);
// 4. 调用核心封禁服务(禁言 3 天)
$result = UserBanService::ban(
$target_uid,
UserBanService::BAN_TYPE_SILENCE,
86400 * 3, // 3 天
$reason,
$uid // 当前登录管理员 uid(全局变量)
);
if($result['code'] != 0) {
message(-1, $result['message']);
}
message(0, lang('user_ban_success'));
3.3 在插件中检查 IP
// 加载依赖
if(!class_exists('IpBlacklistService')) {
include_once APP_PATH.'lib/security/IpBlacklistService.php';
}
// 检查当前访客 IP 是否在黑名单
global $ip;
if(IpBlacklistService::is_blacklisted($ip)) {
message(-1, lang('user_ban_ip_banned'));
}
// 也可以检查任意 IP
if(IpBlacklistService::is_blacklisted('192.168.1.100')) {
// 命中
}
// 检查是否在白名单
if(IpBlacklistService::is_whitelisted($ip)) {
// 白名单优先,跳过检查
}
3.4 在插件中添加 IP 到黑名单
if(!class_exists('IpBlacklistService')) {
include_once APP_PATH.'lib/security/IpBlacklistService.php';
}
// 添加单 IP,永久,备注「广告 IP」
IpBlacklistService::add_blacklist_entry(
'192.168.1.100',
'广告 IP',
0, // 0 = 永久
$uid // 操作管理员 uid
);
// 添加 CIDR 网段,7 天后过期
IpBlacklistService::add_blacklist_entry(
'192.168.1.0/24',
'段封禁',
time() + 86400 * 7, // 过期时间戳
$uid
);
// 添加 IP 范围
IpBlacklistService::add_blacklist_entry(
'192.168.1.1-192.168.1.10',
'范围封禁',
0,
$uid
);
// 从黑名单移除(参数必须与添加时完全一致)
IpBlacklistService::remove_from_blacklist('192.168.1.100');
// 获取黑名单分页列表(自动过滤已过期)
$list = IpBlacklistService::get_blacklist_page(1, 50, true);
$total = IpBlacklistService::count_blacklist(true);
// 清理所有已过期条目(可选,定期调用释放空间)
$cleaned = IpBlacklistService::purge_expired_blacklist();
3.5 监听封禁事件
通过 XnEvent 机制接入封禁流程。文件位置:lib/XnEvent.php。
// plugin/my_plugin/hook/model_inc_start.php
<?php exit;
// 加载依赖
if(!class_exists('UserBanService')) {
include_once APP_PATH.'lib/UserBanService.php';
}
if(!class_exists('XnEvent')) {
include_once APP_PATH.'lib/XnEvent.php';
}
// 监听封禁后事件(如审计日志)
XnEvent::on('UserBanService.afterBan', 'my_plugin', function(&$args) {
// $args 含:uid / banType / duration / reason / adminUid / bannedUntil
db_insert('my_plugin_audit_log', array(
'uid' => $args['uid'],
'action' => 'ban',
'ban_type' => $args['banType'],
'duration' => $args['duration'],
'reason' => $args['reason'],
'admin_uid' => $args['adminUid'],
'expire_at' => $args['bannedUntil'],
'create_time'=> time(),
));
});
// 监听解封后事件(如清理插件数据)
XnEvent::on('UserBanService.afterUnban', 'my_plugin', function(&$args) {
// $args 含:uid / reason / adminUid
db_delete('my_plugin_ban_data', array('uid' => $args['uid']));
});
可用事件清单:
事件名 | 触发时机 | 可修改参数 |
|---|---|---|
| 封禁前 |
|
| 封禁后 | 只读,含 |
| 解封前 |
|
| 解封后 | 只读 |
| 清空前 |
|
| 清空后 | 只读 |
| 公示页渲染时 |
|
3.6 修改封禁时长(风控插件示例)
// plugin/risk_control/hook/model_inc_start.php
<?php exit;
if(!class_exists('XnEvent')) {
include_once APP_PATH.'lib/XnEvent.php';
}
XnEvent::on('UserBanService.beforeBan', 'risk_control', function(&$args) {
// 检查用户风险等级
$risk = RiskControlService::getRiskLevel($args['uid']);
if($risk === 'high' && $args['duration'] > 0 && $args['duration'] < 86400 * 30) {
// 高风险用户:自动延长到 30 天
$args['duration'] = 86400 * 30;
$args['reason'] .= '(风控自动延长至30天)';
}
});
3.7 修改封禁公示页列表
// plugin/my_plugin/hook/model_inc_start.php
<?php exit;
if(!class_exists('XnEvent')) {
include_once APP_PATH.'lib/XnEvent.php';
}
XnEvent::on('UserBanService.bannedListDisplay', 'my_plugin', function(&$args) {
// $args['current_list'] 当前封禁列表(引用)
// $args['recent_list'] 近期释放列表(引用)
foreach($args['current_list'] as &$row) {
$row['plugin_badge'] = '⚠️'; // 添加插件徽章
}
});
3.8 自定义 IP 检查(外接 IP 信誉库)
// plugin/ip_reputation/hook/banned_ip_check.php
<?php exit;
// 此 hook 在 banned_ip_check() 落地后调用
// 如需更严格的检查,可直接调用 IpBlacklistService::is_blacklisted() 或自建查询
$reputation = IpReputationService::query($ip);
if($reputation['score'] < -50) {
// 自动加入本地黑名单
if(!class_exists('IpBlacklistService')) {
include_once APP_PATH.'lib/security/IpBlacklistService.php';
}
IpBlacklistService::add_blacklist_entry(
$ip, $ip,
'IP信誉库自动拦截',
time() + 86400 * 7,
0 // 系统
);
message(-1, lang('user_ban_ip_banned'));
}
3.9 插件卸载时清理事件监听器
重要:插件卸载时必须清理事件监听器,否则卸载后回调仍会被触发,导致 Class not found 错误。
// plugin/my_plugin/uninstall.php
<?php
!defined('DEBUG') AND exit('Access Denied');
if(!class_exists('XnEvent')) {
include_once APP_PATH.'lib/XnEvent.php';
}
// 移除本插件注册的所有事件监听器
XnEvent::off(null, 'my_plugin');
3.10 插件可用的 PHP hook 点
Hook 文件 | 位置 | 用途 |
|---|---|---|
|
| 自定义封禁检查(如第三方风控插件判定) |
|
| 自定义 IP 检查(如外接 IP 信誉库) |
|
| 修改封禁公示页列表数据 |
3.11 插件可用的模板 hook
Hook 名 | 模板位置 | 用途 |
|---|---|---|
|
| 公示页头部注入 |
|
| 公示页中栏顶部注入 |
|
| 添加自定义 tab |
| 当前封禁 tab 内容前 | 注入内容 |
| 当前封禁 tab 内容后 | 注入内容 |
| 近期释放 tab 内容前 | 注入内容 |
| 近期释放 tab 内容后 | 注入内容 |
| 列表项开始 | 修改单项显示 |
| 列表项结束 | 修改单项显示 |
| 列表项开始 | 修改单项显示 |
| 列表项结束 | 修改单项显示 |
| 公示页底部 | 注入内容 |
| 公示页结尾 | 注入内容 |
| 公示页 JS 之后 | 注入自定义脚本 |
四、API 速查表
4.1 UserBanService 静态方法
方法 | 用途 | 返回值 |
|---|---|---|
| 封禁用户 |
|
| 解封用户 |
|
| 检查状态(含自动解封) |
|
| 按场景检查 |
|
| 清空内容 |
|
| 获取格式化状态 |
|
| 类型标签 |
|
| 时长格式化 |
|
| 过期时间格式化 |
|
4.2 UserBanService 常量
常量 | 值 | 说明 |
|---|---|---|
| 0 | 正常 |
| 1 | 禁言 |
| 2 | 禁止访问 |
| 3 | 锁定 |
| 9999999999 | 永久封禁时间戳(约 2286 年) |
|
| 管理员组 gid |
4.3 场景检查规则
Scene | 拒绝的 ban_type | 用途 |
|---|---|---|
| 2, 3 | 禁止访问 / 锁定 不能登录 |
| 2, 3 | 禁止访问 / 锁定 不能浏览 |
| 1, 2, 3 | 禁言及以上不能发帖回帖编辑 |
| 3 | 锁定 不能改密找密 |
4.4 IpBlacklistService 静态方法
方法 | 用途 |
|---|---|
| 检查 IP 是否在黑名单 |
| 检查 IP 是否在白名单 |
| 添加黑名单 |
| 添加白名单 |
| 从黑名单移除 |
| 从白名单移除 |
| 获取全部黑名单 |
| 获取全部白名单 |
| 分页获取黑名单 |
| 黑名单计数 |
| 清理已过期条目,返回清理数量 |
五、测试清单
部署封禁系统后建议测试以下场景:
5.1 基础功能
后台编辑用户 → 选择禁言 7 天 → 提交 → 用户登录前台发帖被拒
禁言用户可登录浏览,但发帖/回帖/编辑被拒
禁止访问用户登录被拒,浏览被拒
锁定用户登录、改密、找密全部被拒
设置封禁时长 1 分钟 → 等待过期 → 再次访问 → 自动解封
永久封禁 →
banned_until=9999999999→ 永不解封
5.2 权限与豁免
尝试封禁 gid=1,2 用户 → 拒绝
管理员尝试封禁自己 → 拒绝
管理员访问被封 IP 不受影响
版主尝试永久封禁 → 拒绝
版主尝试禁言 8 天 → 拒绝
版主尝试禁止访问 → 拒绝
5.3 内容显示
被封用户发的帖子,普通用户看到占位提示
管理员看到原始内容
被封用户本人看到自己的内容
解封后帖子内容自动恢复显示
清空内容后用户主页显示「已被管理员清空内容」
5.4 IP 黑名单
添加单 IP → 用该 IP 访问 → 登录/注册/发帖三个入口全部被拒
添加 CIDR 网段 → 该网段 IP 被拒
添加 IP 范围 → 范围内 IP 被拒
添加白名单 IP → 同时在黑白名单 → 允许访问
设置过期时间 → 过期后自动失效
5.5 公示页
开启
ban_show_public_list→ 访问/banned看到三栏布局切换 tab → URL 同步变为
/banned-current//banned-recent刷新
/banned/recent→ 直接显示「近期释放」tab浏览器前进/后退 → tab 正确还原
关闭
ban_show_public_list→ 访问/banned返回 404
5.6 后台管理
后台「安全设置 → 封禁用户」tab 显示当前封禁用户列表
用户名模糊搜索正常
UID 精确搜索正常
点击解封按钮 → 确认 → 用户从列表消失
后台「安全设置 → IP 黑名单」tab 显示 IP 列表
添加/删除 IP 正常
后台侧栏高亮正确(封禁用户/IP 黑名单都高亮「安全设置」)
5.7 生产环境
DEBUG=0 模式下访问各入口,确认
UserBanService类加载正常DEBUG=0 模式下访问
/banned,确认无Class not found错误多次封禁解封后,后台
user-ban_log-{uid}.htm显示完整历史
六、常见问题
Q1: 封禁后用户还能登录怎么办?
检查封禁类型:
禁言(type=1)允许登录,仅拦截发帖/回帖/编辑
禁止访问(type=2)和锁定(type=3)才会拒绝登录
Q2: 封禁过期后用户状态没恢复?
系统在用户访问时自动检查 banned_until 并自动解封,不需要 cron。如果用户一直不访问,状态会保持到下次访问时才解封。可手动在后台「封禁用户」tab 解封。
Q3: 封禁公示页 404?
检查 conf/conf.php 中是否有 ban_show_public_list 配置项:
没有此项 → 默认开启(代码用
isset() && empty()判断)设为
0→ 关闭,访问返回 404设为
1→ 开启
Q4: 插件中调用 UserBanService 报 Class not found?
生产环境(DEBUG=0)走 tmp/model.min.php 合并加载,类加载顺序不可预测。必须在调用前 include_once:
if(!class_exists('UserBanService')) {
include_once APP_PATH.'lib/UserBanService.php';
}
Q5: IP 黑名单不生效?
检查覆盖入口:
登录(
route/user.phplogin)注册(
route/user.phpcreate)发主题(
route/thread.phpcreate)回帖/编辑不在覆盖范围内(仅拦截发主题入口)
如果需要更多入口拦截,可通过 banned_ip_check.php hook 自行扩展。
Q6: 版主封禁时报错「权限不足」?
版主封禁有严格三重校验:
只能禁言(type=1)
不能永久(duration≠0)
时长必须在 1-7 天(86400-604800 秒)
如果需要更长时长或禁止访问,需管理员操作。
Q7: 清空内容后帖子还在?
清空内容只删除:
回帖(post 表)
主题索引(mythread 表,不删 thread 表主题记录)
附件
通知
重置计数
主题记录本身不会删除,这是已知天花板(避免破坏帖子楼层引用)。如需彻底删除主题,需在后台「主题管理」单独删除。
Q8: 如何在插件中添加自定义封禁检查?
通过 user_ban_check.php hook:
// plugin/my_plugin/hook/user_ban_check.php
<?php exit;
// 此 hook 在核心封禁检查之后调用
// $uid 是当前用户 uid
if(my_plugin_custom_check($uid)) {
message(-1, '您的账号存在风险,已被限制');
}


