封禁系统与 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 同时在黑白名单时,白名单生效,允许访问
过期时间:可设永久或指定过期时间戳,过期后自动失效(不主动清理,查询时跳过)
覆盖入口:登录、注册、发主题三个入口会做 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 内容清空
针对需要保留账号但清空所有内容的场景:
二、管理员使用教程
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 | 用途 |
|---|
发帖限制 | ?security-post_limit.htm
| 发帖/回帖间隔、字数限制 |
账号安全 | ?security-account.htm
| 密码强度、注册间隔 |
内容权限 | ?security-content.htm
| 编辑/删除权限 |
其他设置 | ?security-other.htm
| 软删除、签名修改限制 |
验证码 | ?security-captcha.htm
| 验证码配置 |
敏感词 | ?security-words.htm
| 敏感词过滤 |
IP 黑名单 | ?banned_ip-list.htm
| IP 黑白名单管理 |
封禁用户 | ?banned_user-list.htm
| 当前封禁用户列表/解封 |
2.3 封禁用户流程
方式 1:从用户列表进入
后台「用户管理 → 用户列表」
找到目标用户,点击「编辑」
在「封禁设置」区域:
提交即可
方式 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 命中后的行为
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']));
});
可用事件清单:
事件名 | 触发时机 | 可修改参数 |
|---|
UserBanService.beforeBan
| 封禁前 | banType / duration / reason(引用,可修改)
|
UserBanService.afterBan
| 封禁后 | 只读,含 bannedUntil |
UserBanService.beforeUnban
| 解封前 | reason(引用,可修改)
|
UserBanService.afterUnban
| 解封后 | 只读 |
UserBanService.beforeClearContent
| 清空前 | uid / adminUid
|
UserBanService.afterClearContent
| 清空后 | 只读 |
UserBanService.bannedListDisplay
| 公示页渲染时 | current_list / recent_list(引用,可修改)
|
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 文件 | 位置 | 用途 |
|---|
user_ban_check.php
| route/user.php / route/thread.php / route/post.php / route/my.php / index.inc.php
| 自定义封禁检查(如第三方风控插件判定) |
banned_ip_check.php
| route/user.php login/create / route/thread.php create
| 自定义 IP 检查(如外接 IP 信誉库) |
banned_list_display.php
| route/banned.php
| 修改封禁公示页列表数据 |
3.11 插件可用的模板 hook
Hook 名 | 模板位置 | 用途 |
|---|
banned_start.htm
| banned.htm 顶部
| 公示页头部注入 |
banned_left_before.htm
| banned.htm 中栏顶部
| 公示页中栏顶部注入 |
banned_tab_item_after.htm
| banned.htm nav-tabs 末尾
| 添加自定义 tab |
banned_current_body_before.htm
| 当前封禁 tab 内容前 | 注入内容 |
banned_current_body_after.htm
| 当前封禁 tab 内容后 | 注入内容 |
banned_recent_body_before.htm
| 近期释放 tab 内容前 | 注入内容 |
banned_recent_body_after.htm
| 近期释放 tab 内容后 | 注入内容 |
banned_current_item_start.htm
| 列表项开始 | 修改单项显示 |
banned_current_item_end.htm
| 列表项结束 | 修改单项显示 |
banned_recent_item_start.htm
| 列表项开始 | 修改单项显示 |
banned_recent_item_end.htm
| 列表项结束 | 修改单项显示 |
banned_bottom.htm
| 公示页底部 | 注入内容 |
banned_end.htm
| 公示页结尾 | 注入内容 |
banned_js.htm
| 公示页 JS 之后 | 注入自定义脚本 |
四、API 速查表
4.1 UserBanService 静态方法
方法 | 用途 | 返回值 |
|---|
UserBanService::ban($uid, $banType, $duration, $reason, $adminUid)
| 封禁用户 | ['code'=>0成功, 'message'=>错误]
|
UserBanService::unban($uid, $adminUid, $reason='')
| 解封用户 | ['code'=>0, 'message'=>]
|
UserBanService::checkBan($uid)
| 检查状态(含自动解封) | ['banned'=>bool, 'ban_type'=>, 'ban_reason'=>, 'expire_time'=>, 'expire_formatted'=>]
|
UserBanService::checkBanByScene($uid, $scene)
| 按场景检查 | ['allowed'=>bool, 'message'=>]
|
UserBanService::clearContent($uid, $adminUid)
| 清空内容 | ['code'=>0, 'message'=>]
|
UserBanService::getBanStatus($uid)
| 获取格式化状态 | ['ban_type', 'ban_reason', 'ban_time', 'banned_until', 'expire_formatted', 'status_label', 'status_color']
|
UserBanService::getBanTypeLabel($banType)
| 类型标签 | ['label'=>, 'color'=>]
|
UserBanService::formatDuration($duration)
| 时长格式化 | "7天" / "永久" / "3小时"
|
UserBanService::formatExpireTime($banned_until)
| 过期时间格式化 | "2026-07-09 12:00:00" / "永久"
|
4.2 UserBanService 常量
常量 | 值 | 说明 |
|---|
BAN_TYPE_NORMAL
| 0 | 正常 |
BAN_TYPE_SILENCE
| 1 | 禁言 |
BAN_TYPE_BAN_ACCESS
| 2 | 禁止访问 |
BAN_TYPE_LOCK
| 3 | 锁定 |
PERMANENT_BAN
| 9999999999 | 永久封禁时间戳(约 2286 年) |
ADMIN_GIDS
| [1, 2]
| 管理员组 gid |
4.3 场景检查规则
Scene | 拒绝的 ban_type | 用途 |
|---|
login
| 2, 3 | 禁止访问 / 锁定 不能登录 |
browse
| 2, 3 | 禁止访问 / 锁定 不能浏览 |
post
| 1, 2, 3 | 禁言及以上不能发帖回帖编辑 |
password
| 3 | 锁定 不能改密找密 |
4.4 IpBlacklistService 静态方法
方法 | 用途 |
|---|
IpBlacklistService::is_blacklisted(string $ip): bool
| 检查 IP 是否在黑名单 |
IpBlacklistService::is_whitelisted(string $ip): bool
| 检查 IP 是否在白名单 |
IpBlacklistService::add_blacklist_entry(string $ip, string $remark='', int $expire_time=0, int $admin_uid=0): bool
| 添加黑名单 |
IpBlacklistService::add_whitelist_entry(string $ip, string $remark='', int $expire_time=0, int $admin_uid=0): bool
| 添加白名单 |
IpBlacklistService::remove_from_blacklist(string $ip): bool
| 从黑名单移除 |
IpBlacklistService::remove_from_whitelist(string $ip): bool
| 从白名单移除 |
IpBlacklistService::get_blacklist(): array
| 获取全部黑名单 |
IpBlacklistService::get_whitelist(): array
| 获取全部白名单 |
IpBlacklistService::get_blacklist_page(int $page=1, int $pagesize=50, bool $exclude_expired=false): array
| 分页获取黑名单 |
IpBlacklistService::count_blacklist(bool $exclude_expired=false): int
| 黑名单计数 |
IpBlacklistService::purge_expired_blacklist(): int
| 清理已过期条目,返回清理数量 |
五、测试清单
部署封禁系统后建议测试以下场景:
5.1 基础功能
后台编辑用户 → 选择禁言 7 天 → 提交 → 用户登录前台发帖被拒
禁言用户可登录浏览,但发帖/回帖/编辑被拒
禁止访问用户登录被拒,浏览被拒
锁定用户登录、改密、找密全部被拒
设置封禁时长 1 分钟 → 等待过期 → 再次访问 → 自动解封
永久封禁 → banned_until=9999999999 → 永不解封
5.2 权限与豁免
尝试封禁 gid=1,2 用户 → 拒绝
管理员尝试封禁自己 → 拒绝
管理员访问被封 IP 不受影响
版主尝试永久封禁 → 拒绝
版主尝试禁言 8 天 → 拒绝
版主尝试禁止访问 → 拒绝
5.3 内容显示
被封用户发的帖子,普通用户看到占位提示
管理员看到原始内容
被封用户本人看到自己的内容
解封后帖子内容自动恢复显示
清空内容后用户主页显示「已被管理员清空内容」
5.4 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: 封禁后用户还能登录怎么办?
检查封禁类型:
Q2: 封禁过期后用户状态没恢复?
系统在用户访问时自动检查 banned_until 并自动解封,不需要 cron。如果用户一直不访问,状态会保持到下次访问时才解封。可手动在后台「封禁用户」tab 解封。
Q3: 封禁公示页 404?
检查 conf/conf.php 中是否有 ban_show_public_list 配置项:
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 黑名单不生效?
检查覆盖入口:
如果需要更多入口拦截,可通过 banned_ip_check.php hook 自行扩展。
Q6: 版主封禁时报错「权限不足」?
版主封禁有严格三重校验:
只能禁言(type=1)
不能永久(duration≠0)
时长必须在 1-7 天(86400-604800 秒)
如果需要更长时长或禁止访问,需管理员操作。
Q7: 清空内容后帖子还在?
清空内容只删除:
主题记录本身不会删除,这是已知天花板(避免破坏帖子楼层引用)。如需彻底删除主题,需在后台「主题管理」单独删除。
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, '您的账号存在风险,已被限制');
}