1. 概述
用户封禁系统是 Xiuno BBS 4.5+ 的核心安全模块,提供 4 档用户状态管理,用于对付恶意用户(小黑子)。系统采用「核心实现 + 事件扩展」架构:核心逻辑在 lib/UserBanService.php,插件通过 lib/XnEvent.php 事件机制接入。
核心特性:
4 档封禁状态:正常 / 禁言 / 禁止访问 / 锁定
场景化检查:login / browse / post / password 四种入口分别拦截不同 ban_type
到期自动解封:访问时检查 banned_until,过期自动重置字段
封禁历史:完整记录 ban / unban / auto_unban / clear_content 操作
IP 黑名单:支持单 IP 与 IP 段(范围)封禁
版主权限分级:版主仅能禁言 1-7 天,管理员可永久 / 锁定
内容清空:保留账号、清空回帖/主题索引/附件/通知
前台内容隐藏:被封用户发布的内容显示占位提示,管理员可见原始内容
封禁公示页:双栏展示当前封禁与近期释放用户
事件机制:6 个 XnEvent 事件 + 3 个 PHP hook 点供插件扩展
设计原则:
永久封禁时间戳用 9999999999(约 2286 年),避免 32 位系统 PHP_INT_MAX 溢出
不可封禁管理员组(gid=1,2)和封禁操作者自己
ban_type 等字段不在 USER_UPDATE_PROTECTED_FIELDS 中,可用 user_update() 而非 user__update(),前者会自动清缓存 + 触发 hook
通知函数是 notify_create()(非 notice_send()),用 type='system' + from_uid=0 允许系统通知自己
2. 数据库设计
2.1 user 表扩展字段
ALTER TABLE `bbs_user`
ADD COLUMN `ban_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '封禁类型 0正常/1禁言/2禁止访问/3锁定',
ADD COLUMN `ban_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '封禁原因',
ADD COLUMN `ban_admin_uid` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '封禁操作管理员uid',
ADD COLUMN `ban_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '封禁时间';
-- 复用已有 banned_until 字段:0=未封禁,9999999999=永久,其他=到期时间戳
字段 | 类型 | 说明 |
|---|
ban_type | tinyint(1) | 0 正常 / 1 禁言 / 2 禁止访问 / 3 锁定 |
banned_until | int(11) unsigned | 复用已有字段。0=未封禁,9999999999=永久,其他=到期 Unix 时间戳 |
ban_reason | varchar(255) | 封禁原因,显示给用户和后台 |
ban_admin_uid | int(11) unsigned | 执行封禁的管理员 uid(系统自动解封时为 0) |
ban_time | int(11) unsigned | 最近一次封禁时间 |
2.2 bbs_user_ban_log 表(封禁历史)
CREATE TABLE `bbs_user_ban_log` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '被操作用户uid',
`admin_uid` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '操作管理员uid(0=系统自动)',
`action` varchar(16) NOT NULL DEFAULT '' COMMENT 'ban/unban/auto_unban/clear_content',
`ban_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '封禁类型(unban/auto_unban/clear_content时为0)',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '原因',
`duration` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '封禁时长秒数(0=永久)',
`create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '操作时间',
PRIMARY KEY (id),
KEY uid (uid),
KEY action_time (action, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
action 取值:
2.3 bbs_banned_ip 表(IP 黑名单)
CREATE TABLE `bbs_banned_ip` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`ip_start` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '起始IP(ip2long整型)',
`ip_end` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '结束IP(ip2long整型)',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '封禁原因',
`admin_uid` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '操作管理员uid',
`create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
`expire_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '过期时间戳(0=永久)',
PRIMARY KEY (id),
KEY ip_range (ip_start, ip_end)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:ip_start/ip_end 字段为整型,写入必须用 ip2long() + sprintf('%u', ...) 处理 32 位系统溢出。读取显示时用 long2ip() 还原。
3. 配置项
在 conf/conf.default.php 中:
配置项 | 类型 | 默认值 | 说明 |
|---|
ban_show_public_list
| int | 1 | 是否开启封禁公示页(/banned)。0 关闭(访问返回 404),1 开启 |
ban_inherit_to_same_ip
| int | 0 | 预留:是否对同 IP 用户继承封禁状态。当前未启用,供插件实现 |
4. UserBanService API
文件位置:lib/UserBanService.php
4.1 常量
UserBanService::BAN_TYPE_NORMAL = 0; // 正常
UserBanService::BAN_TYPE_SILENCE = 1; // 禁言(可浏览,不能发帖回帖)
UserBanService::BAN_TYPE_BAN_ACCESS = 2; // 禁止访问(不能登录、不能浏览)
UserBanService::BAN_TYPE_LOCK = 3; // 锁定(不能登录、不能改密找密)
UserBanService::PERMANENT_BAN = 9999999999; // 永久封禁时间戳(约2286年)
UserBanService::ADMIN_GIDS = [1, 2]; // 管理员组 gid(不可被封禁/清空内容)
4.2 静态方法
ban($uid, $banType, $duration, $reason, $adminUid) — 封禁用户
$result = UserBanService::ban(
$uid, // 被封禁用户 uid
UserBanService::BAN_TYPE_SILENCE, // 封禁类型 1/2/3
86400 * 7, // 时长秒数,0=永久
'广告灌水', // 封禁原因
$adminUid // 操作管理员 uid
);
// 返回 ['code'=>0 成功, 'message'=>错误信息]
校验:
$uid > 0 且 $adminUid > 0
$banType 必须是 1/2/3
$duration >= 0(0=永久)
不能封禁自己($uid === $adminUid)
不能封禁管理员组(gid ∈ ADMIN_GIDS)
user 表必须有 ban_type 字段(升级后才有)
流程:触发 beforeBan 事件 → 更新 user 表 → 写 ban_log(action=ban)→ 发送通知 → 触发 afterBan 事件。
unban($uid, $adminUid, $reason = '') — 解封用户
$result = UserBanService::unban($uid, $adminUid, '申诉通过');
流程:触发 beforeUnban → 重置 5 个封禁字段为 0/空 → 写 ban_log(action=unban)→ 发送通知 → 触发 afterUnban。
checkBan($uid) — 检查封禁状态(含到期自动解封)
$status = UserBanService::checkBan($uid);
// 返回 ['banned'=>bool, 'ban_type'=>int, 'ban_reason'=>string, 'expire_time'=>int, 'expire_formatted'=>string]
自动解封:若 banned_until > 0 且 <= time(),自动调用 autoUnban() 重置字段并写日志(action=auto_unban,不发通知),返回 banned=false。
checkBanByScene($uid, $scene) — 按场景检查(用于入口拦截)
$check = UserBanService::checkBanByScene($uid, 'post');
if(!$check['allowed']) {
message(-1, $check['message']);
}
场景规则:
Scene | 拒绝的 ban_type | 用途 |
|---|
login
| 2, 3 | 禁止访问 / 锁定 不能登录 |
browse
| 2, 3 | 禁止访问 / 锁定 不能浏览 |
post
| 1, 2, 3 | 禁言及以上不能发帖回帖编辑 |
password
| 3 | 锁定 不能改密找密 |
其他 | 1, 2, 3 | 未知场景保守拒绝所有 |
clearContent($uid, $adminUid) — 清空用户内容(保留账号)
$result = UserBanService::clearContent($uid, $adminUid);
清理范围:
删除所有回帖(post_delete_by_uid)
删除主题索引(mythread_delete_by_uid,不删 thread 表主题记录,已知天花板见代码注释)
删除附件(attach_delete_by_uid)
删除通知(notify_delete_by_uid)
重置 threads / posts 计数为 0
流程:触发 beforeClearContent → 删除内容 → 重置计数 → 写 ban_log(action=clear_content)→ 触发 afterClearContent。不发通知。
getBanStatus($uid) — 获取格式化封禁状态(前端显示用)
$status = UserBanService::getBanStatus($uid);
// 返回 ['ban_type', 'ban_reason', 'ban_time', 'banned_until',
// 'expire_formatted', 'status_label', 'status_color']
status_color 用于 Bootstrap 徽章:success(正常)/ warning(禁言)/ danger(禁止访问)/ dark(锁定)。
辅助方法
UserBanService::getBanTypeLabel($banType); // 返回 ['label'=>..., 'color'=>...]
UserBanService::formatDuration($duration); // "7天" / "永久" / "3小时"
UserBanService::formatExpireTime($banned_until); // "2026-07-09 12:00:00" / "永久"
5. 模型函数
5.1 ban_log.func.php(封禁历史)
ban_log_create($data) // 创建记录(自动补 create_time)
ban_log_find_by_uid($uid, $page=1, $ps=20) // 按 uid 查询
ban_log_count_by_uid($uid) // 按 uid 计数
ban_log_find_all($cond=[], $page=1, $ps=50) // 后台查询所有
ban_log_delete_by_uid($uid) // 删除某用户所有记录(用户被彻底删除时调用)
ban_log_find_recent_unbanned($days=30, $lim=20) // 近 N 天解封记录(公示页用)
5.2 banned_ip.func.php(IP 黑名单)
banned_ip_create($ip_start, $ip_end, $reason, $admin_uid, $expire_time=0) // 创建
banned_ip_delete($id) // 删除
banned_ip_find($page=1, $pagesize=50) // 列表
banned_ip_count() // 计数
banned_ip_check($ip) // 检查 IP 是否命中(已废弃,转发到 IpBlacklistService::is_blacklisted())
banned_ip_read($id) // 单条查询
新代码请直接用 IpBlacklistService::is_blacklisted($ip)(位于 lib/security/IpBlacklistService.php),banned_ip_check() 仅保留兼容。
6. 后台管理
6.1 用户管理扩展(admin/route/user.php)
Action | 用途 |
|---|
list
| 用户列表,支持按 ban_type 筛选,显示状态徽章、批量操作 Modal、行内一键禁言7天按钮 |
update
| 用户编辑页含「封禁设置」区(类型/时长/原因),已封禁显示当前状态并支持修改 |
ban
| POST 封禁(POST 处理在 header include 之前) |
unban
| POST 解封 |
clear_content
| POST 清空内容 |
ban_log
| 查看某用户封禁历史 |
6.2 IP 黑名单(admin/route/banned_ip.php)
后台菜单「其他 → IP 黑名单」,支持单 IP / IP 段新增、过期时间设置、删除。
6.3 升级
部署后必须到 /admin/?upgrade.htm 执行「用户封禁系统」升级项,会:
为 user 表添加 4 个新字段
创建 bbs_user_ban_log 表
创建 bbs_banned_ip 表
为已存在用户初始化 ban_type=0
7. 核心检查点
入口 | 文件 | 检查内容 |
|---|
全局 | index.inc.php
| ban_type=2,3 跳转封禁提示页(admin 豁免用 SCRIPT_NAME,管理员组豁免,AJAX 返回 JSON) |
登录 | route/user.php login
| ban_type=2,3 拒绝登录 + IP 黑名单 |
注册 | route/user.php create
| IP 黑名单 |
找密 | route/user.php resetpw
| ban_type=3 锁定拒绝 |
改密 | route/my.php password
| ban_type=3 锁定拒绝(改密在 my.php 不在 user.php) |
发主题 | route/thread.php create
| ban_type>0 拒绝 + IP 黑名单 |
发回帖 | route/post.php create
| ban_type>0 拒绝 |
编辑 | route/post.php update
| ban_type>0 拒绝 |
封禁提示页:view/htm/banned_notice.htm 显示封禁原因、解封时间、倒计时、申诉链接、退出登录按钮。
8. 前台内容显示
被封用户发布的内容会被隐藏,显示占位提示「该用户被关禁闭,内容被隐藏」。涉及的模板:
模板 | 处理 |
|---|
view/htm/thread_main.inc.htm
| 楼主主题内容隐藏 + 状态徽章 + 封禁按钮(版主/管理员可见) |
view/htm/post_list.inc.htm
| 回帖内容隐藏 + 状态徽章 + 封禁按钮 |
view/htm/thread_list.inc.htm
| 帖子标题前 [已隐藏] 标记 |
view/htm/user_info_card.inc.htm
| 用户主页状态徽章 + 本人显示完整封禁信息 + 清空内容提示 |
复用函数(model/misc.func.php):
特殊规则:
9. 封禁公示页
路由:/banned(route/banned.php)
模板:view/htm/banned.htm,双栏布局(当前封禁中 / 近期释放30天)
受 ban_show_public_list 配置控制,关闭时返回 404
预留 hook 点:banned_list_display.php + XnEvent::trigger('UserBanService.bannedListDisplay')
10. 版主封禁入口
路由:route/mod.php ban_user action
权限:管理员(gid=1,2)直接放行;版主需 forum_access_mod($fid, $gid, 'allowbanuser')
版主权限限制(三重校验):
ban_type 必须为 1(禁言)
duration 不能为 0(不能永久)
duration 必须在 [86400, 604800](1-7天)
管理员可永久封禁和锁定
前台帖子页作者卡片「封禁」按钮(thread_main.inc.htm + post_list.inc.htm)+ 封禁弹窗 Modal
路由 URL:mod_ban_user_url()(model/route.func.php)
11. xnx_report 举报系统联动
lib/security/ReportService.php 的 handle_report() / batch_handle() 中:
ban 分支:调用 UserBanService::ban($target_uid, BAN_TYPE_SILENCE, 604800, $reason, $handler_uid) 默认禁言 7 天
delete_ban 分支:先删内容再封禁(封禁失败时保持已处理状态,提示「内容已删除,但封禁失败」)
封禁失败回滚:纯 ban 操作失败时回滚举报状态为待处理
复用现有「封禁用户」「删除并封禁」按钮 UI,不新增重复按钮
12. 插件扩展接口
12.1 XnEvent 事件机制
文件位置:lib/XnEvent.php,提供 on / once / trigger / off / hasListeners 五个静态方法。
事件名约定:ClassName.methodName,如 UserBanService.beforeBan。
回调签名:function(&$args)(参数按引用传递,可修改后传递给主流程)。
异常处理:回调抛出异常不会中断主流程,仅记录 xn_log('error')。
12.2 UserBanService 触发的 7 个事件
事件名 | 触发时机 | 可修改参数 |
|---|
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(引用,可修改)
|
12.3 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
| 修改封禁公示页列表数据 |
12.4 插件调用示例
示例 1:监听封禁事件,记录到第三方日志系统
// plugin/my_audit/hook/model_inc_start.php
<?php exit;
if(!class_exists('UserBanService')) { include_once APP_PATH.'lib/UserBanService.php'; }
XnEvent::on('UserBanService.afterBan', 'my_audit', function(&$args) {
// $args 含 uid/banType/duration/reason/adminUid/bannedUntil
// 写入第三方审计系统(如 Elasticsearch)
my_audit_log_write('user_ban', array(
'uid' => $args['uid'],
'ban_type' => $args['banType'],
'duration' => $args['duration'],
'reason' => $args['reason'],
'admin_uid' => $args['adminUid'],
'expire_at' => $args['bannedUntil'],
));
});
示例 2:封禁前修改封禁时长(如风控插件自动延长)
// plugin/risk_control/hook/model_inc_start.php
<?php exit;
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:插件中调用 UserBanService 封禁用户
// plugin/my_plugin/route/my_plugin.php
<?php
!defined('DEBUG') AND exit('Access Denied');
// 必须先 include_once(生产环境 min.php 类加载顺序不可预测)
if(!class_exists('UserBanService')) {
include_once APP_PATH.'lib/UserBanService.php';
}
if($method == 'POST' && $action == 'auto_ban') {
CsrfService::check();
$target_uid = intval(param('uid'));
$reason = param('reason', '', FALSE); // 第3参数 FALSE 关闭 htmlspecialchars
// 调用核心封禁服务(禁言 3 天)
$result = UserBanService::ban(
$target_uid,
UserBanService::BAN_TYPE_SILENCE,
86400 * 3,
$reason,
$uid // 当前登录管理员 uid
);
if($result['code'] != 0) {
message(-1, $result['message']);
}
message(0, lang('user_ban_success'));
}
示例 4:解封时清理插件自身数据
// plugin/my_plugin/hook/model_inc_start.php
<?php exit;
XnEvent::on('UserBanService.afterUnban', 'my_plugin', function(&$args) {
// 清理插件中存储的该用户封禁期间数据
db_delete('my_plugin_ban_data', array('uid' => $args['uid']));
});
示例 5:自定义 IP 检查(外接 IP 信誉库)
// plugin/ip_reputation/hook/banned_ip_check.php
<?php exit;
// 此 hook 在 banned_ip_check($ip) 落地后调用
// 如需更严格的检查,可直接调用 IpBlacklistService::is_blacklisted($ip) 或自建查询
$reputation = IpReputationService::query($ip);
if($reputation['score'] < -50) {
// 自动加入本地黑名单
if(!class_exists('IpBlacklistService')) {
include_once APP_PATH.'lib/security/IpBlacklistService.php';
}
IpBlacklistService::add($ip, $ip, 'IP信誉库自动拦截', 0, 86400 * 7);
message(-1, lang('user_ban_ip_banned'));
}
示例 6:修改封禁公示页列表
// plugin/my_plugin/hook/model_inc_start.php
<?php exit;
XnEvent::on('UserBanService.bannedListDisplay', 'my_plugin', function(&$args) {
// $args['current_list'] / $args['recent_list'] 是引用
// 添加插件标识的徽章
foreach($args['current_list'] as &$row) {
$row['plugin_badge'] = '⚠️';
}
});
12.5 插件卸载时清理事件监听器
// 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');
13. 多语言包
封禁系统涉及的语言包 key(zh-cn / zh-tw / en-us 三语已同步):
带占位符的 key:
新增功能时必须同步三种语言包,否则用户看到原始 key 名。
14. 安全规范
所有 POST 表单包含 CsrfService::input() + 后端 CsrfService::check()
后台路由 POST 处理在 header.inc.htm include 之前(message() 303 要求)
IP 字段写入用 ip2long() 转整型,禁用 intval("ip字符串")(只返回第一段)
IP / 原因等参数 param() 第 3 参数传 FALSE 关闭 htmlspecialchars
ban_type 等字段不在 USER_UPDATE_PROTECTED_FIELDS 中,用 user_update() 而非 user__update()(前者自动 cache_delete + 触发 hook)
删除操作幂等:对已删除记录返回成功而非 404
admin 检测用 SCRIPT_NAME(兼容子目录安装),非 REQUEST_URI
15. 缓存与编译同步
16. 关键文件清单
类型 | 文件 |
|---|
核心服务 | lib/UserBanService.php
|
事件机制 | lib/XnEvent.php
|
升级服务 | lib/UpgradeService.php(upgradeUserBanSystem() 方法)
|
模型 | model/ban_log.func.php、model/banned_ip.func.php
|
IP 黑名单服务 | lib/security/IpBlacklistService.php
|
全局检查 | index.inc.php
|
路由检查 | route/user.php、route/thread.php、route/post.php、route/my.php
|
封禁提示页 | view/htm/banned_notice.htm
|
公示页 | route/banned.php、view/htm/banned.htm
|
版主入口 | route/mod.php(ban_user action)
|
后台用户管理 | admin/route/user.php、admin/view/htm/user_list.htm、user_update.htm、user_ban_log.htm
|
后台 IP 黑名单 | admin/route/banned_ip.php、admin/view/htm/banned_ip_list.htm
|
举报联动 | lib/security/ReportService.php
|
复用函数 | model/misc.func.php(user_ban_badge_html / user_ban_hidden_notice_html)
|
数据库 | install/install.sql
|
配置 | conf/conf.default.php
|
17. 测试要点
部署后建议测试以下场景:
基础封禁:后台编辑用户 → 选择禁言 7 天 → 提交 → 用户登录前台发帖被拒
场景隔离:禁言用户可登录浏览,但发帖/回帖/编辑被拒;禁止访问用户登录被拒
自动解封:设置封禁时长 1 分钟 → 等待过期 → 再次访问 → 自动解封
永久封禁:duration=0 → banned_until=9999999999 → 永不解封
管理员豁免:尝试封禁 gid=1,2 用户 → 拒绝;管理员自己访问被封 IP 不受影响
不能封禁自己:管理员尝试封禁自己 → 拒绝
版主权限:版主尝试永久封禁 → 拒绝;尝试禁言 8 天 → 拒绝;尝试禁止访问 → 拒绝
内容隐藏:被封用户发的帖子,普通用户看到占位提示,管理员看到原始内容
解封恢复:解封后帖子内容自动恢复显示
清空内容:清空操作 → 该用户主页显示「已被管理员清空内容」
IP 黑名单:添加 IP → 用该 IP 访问 → 登录/注册/发帖三个入口全部被拒
封禁公示页:开启 ban_show_public_list → 访问 /banned 看到双栏列表
封禁历史:多次封禁解封后,后台 user-ban_log-{uid}.htm 显示完整历史
xnx_report 联动:举报 → 选择「封禁用户」→ 用户被禁言 7 天 + 通知
生产环境:DEBUG=0 模式下访问各入口,确认 UserBanService 类加载正常(无 Class not found)