官方不提供现金交易,线下交易风险自担,请仔细鉴别!
🔗 oulin_outlink — 外链安全跳转插件 v1.1.0
Xiuno BBS X 插件 · 作者 oulin 将帖子内容中的外部链接自动替换为安全中转页,显示安全提醒与倒计时后跳转,支持域名白名单直接放行。
一、插件概述
oulin_outlink 是一款为 Xiuno BBS X 论坛设计的外链安全管理插件。当用户在帖子中发布指向外部网站的链接时,插件自动将其替换为站内中转页 URL。访客点击后先进入一个带安全提醒的中间页面,经过可配置的倒计时后跳转目标站。本站域名和自定义白名单域名可直接 302 放行,不经中转页。
解决什么问题?
痛点 | 解决方案 |
|---|---|
恶意链接 / 钓鱼网站 | 中转页显示完整目标 URL,让用户确认后再跳转 |
搜索引擎权重外泄 |
|
外链统计困难 | 每次跳转记录 URL、域名、UID、IP,后台可视化统计 |
合规 / 免责需求 | 支持自定义安全提示文字,声明"即将离开本站" |
可信站点无需中转 | 域名白名单 + 通配符匹配,可信域名直接放行 |
二、工作流程
用户发帖,帖子内容含 <a href="https://external.com">外链</a>
│
▼
① model_post_format_end hook 在 post_format() 帖子格式化完成后触发
│
▼
② OutlinkService::rewriteOutlinks($html)
· 快速预检:stripos($html, '<a ') — 无链接标签直接跳过
· 正则匹配所有 <a> 标签的 href
· 识别外链 → 替换为 /?outlink.htm?url=<base64url编码>
· 本站域名 / 白名单域名 → 保留原始链接不拦截
│
▼
③ 用户点击链接 → 访问 /?outlink.htm?url=xxx
│
├── 本站域名或白名单 → HTTP 302 直跳(无中转页)
│
└── 外部域名 → 显示中转页
├── 安全提醒 + 倒计时(0~60 秒可配)
├── 显示目标 URL(可关闭)
├── 自定义提示文字(可选)
├── X-Robots-Tag + meta robots 双层 noindex
├── 记录跳转日志(URL / 域名 / UID / IP / 时间)
└── 倒计时结束或点击「立即跳转」→ 跳转目标
三、安全机制
防护层 | 实现 |
|---|---|
协议白名单 | 仅允许 |
CR/LF 注入防护 |
|
URL 长度限制 | 编码串 ≤ 4096 字符,解码后 URL ≤ 4096 字符 |
base64url 字符白名单 |
|
host 校验 |
|
noindex 双层保障 | HTTP 头 |
rel 属性 | 跳转按钮 |
CSRF | 后台设置保存和清空日志均带 |
XSS | 所有用户输入(URL、自定义提示)通过 |
参数化查询 | 日志写入和清理使用 |
权限控制 | 后台仅 |
四、性能优化(v1.1.0 新增)
优化 | 效果 |
|---|---|
请求级静态缓存 |
|
快速预检跳过 |
|
协议相对 URL 归一化 |
|
SQL 端聚合统计 | 后台热门域名从「取 500 行 PHP 遍历聚合」改为 |
日志节流清理 | 过期日志清理每小时最多执行一次( |
UTF-8 安全截断 | 显示 URL 超长时用 |
五、后台管理功能
设置项
设置 | 类型 | 范围 | 默认值 | 说明 |
|---|---|---|---|---|
启用外链跳转 | 开关 | 开/关 | ✅ 开 | 总开关,关闭后帖子外链不被改写 |
倒计时秒数 | 数字 | 0~60 | 5 | 中转页倒计时,设为 0 则仅显示「立即跳转」按钮 |
显示目标链接 | 开关 | 开/关 | ✅ 开 | 是否在中转页显示完整目标 URL |
跳转页 noindex | 开关 | 开/关 | ✅ 开 | 防搜索引擎收录中转页 |
自定义提示 | 文本 | 任意 | 空 | 显示在中转页的额外提示,留空用默认提醒 |
域名白名单 | 多行文本 | 域名列表 | 空 | 每行一个域名,支持 |
日志保留天数 | 数字 | 0~3650 | 90 | 超过天数的日志自动清理,0 = 永不自动清理 |
统计概览(后台顶部三卡片)
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ 🖱️ 累计跳转 │ │ 📅 今日跳转 │ │ 📈 热门域名 TOP 10 │
│ 1,234 │ │ 56 │ │ example.com 120 │
│ │ │ │ │ blog.test.org 89 │
└──────────────┘ └──────────────┘ │ ... │
└──────────────────────────┘
累计跳转:
oulin_outlink_log表总行数今日跳转:今日 0 点后的跳转次数
热门域名 Top 10:
GROUP BY domain + COUNT(*)降序排列一键清空日志:底部独立卡片,带 JavaScript
confirm()二次确认 + CSRF 保护
六、中转页 UI
┌─────────────────────────────────────────────┐
│ │
│ 🔗 (外链图标 + 呼吸动画) │
│ │
│ 您即将离开本站 │
│ 即将跳转到外部网站,请确认目标链接安全: │
│ │
│ ┌─────────────────────────────────────────┐│
│ │ 目标链接 ││
│ │ https://www.example.com/very/long/... ││
│ └─────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────┐│
│ │ ℹ️ 管理员自定义提示文字(可选) ││
│ └─────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────┐│
│ │ ⚠️ 请注意保护个人隐私和财产安全... ││
│ └─────────────────────────────────────────┘│
│ │
│ 5 秒后自动跳转... │
│ │
│ [→ 立即跳转] [← 返回上一页] │
│ │
└─────────────────────────────────────────────┘
Bootstrap 5 + Tabler Icons,响应式布局(
col-12 col-md-8 col-lg-6居中卡片)JavaScript:
setInterval倒计时每秒更新文字,归零自动跳转;用户点击「立即跳转」时clearInterval防二次触发无 jQuery / Alpine.js 依赖,纯原生 JS
七、核心类 OutlinkService API
方法 | 说明 |
|---|---|
| 获取插件设置(请求级缓存) |
| 保存设置 + 刷新内存缓存 |
| 获取本站域名列表(从 |
| 解析用户白名单文本为数组(支持注释和通配符,缓存) |
| 判断域名是否在白名单(精确 + |
| 判断是否本站域名(含子域名) |
| 安全校验:协议 + host + CR/LF + 长度 |
| base64url 编码(URL 安全字符,无 padding) |
| base64url 解码(带长度和字符集保护) |
| 正则改写 HTML 中的外链为中转链接 |
| 记录跳转日志 + 节流清理过期日志 |
| 获取日志保留天数(0~3650) |
| 清空全部日志 |
八、插件架构
目录结构
plugin/oulin_outlink/
├── conf.json # 插件元数据(版本/名称/hook 注册)
├── icon.png # 插件图标
├── install.php # 安装:建表 + 默认设置
├── uninstall.php # 卸载:删表 + 清设置
├── setting.php # 后台设置入口(权限校验 + 保存 + 统计)
├── render_icon.py # 图标生成脚本
│
├── model/
│ └── OutlinkService.php # 核心 Service 类(全部业务逻辑)
│
├── route/
│ ├── outlink.php # 前台路由:中转页
│ └── admin_outlink.php # 后台路由:重定向到设置页
│
├── view/htm/
│ ├── outlink.htm # 中转页模板
│ └── setting.htm # 后台设置页模板
│
├── hook/
│ ├── model_inc_file.php # 注册 Service 文件到 model.inc.php
│ ├── model_post_format_end.php # 帖子格式化后改写外链
│ ├── index_route_case_end.php # 注册前台路由 outlink
│ ├── model_route_table_end.php # 注册路由表条目
│ ├── admin_index_route_case_end.php # 注册后台路由 oulin_outlink
│ ├── header_link_after.htm # 注入 CSS 到页面头部
│ └── lang_zh_cn_bbs.php # 中文语言包(38 条语言键)
│
└── static/
├── css/outlink.css # 中转页样式
└── js/outlink.js # 倒计时 + 按钮交互
数据表
CREATE TABLE bbs_oulin_outlink_log (
id int unsigned AUTO_INCREMENT,
url varchar(2048), -- 目标 URL(截断至 2048)
domain varchar(255), -- 目标域名
uid int unsigned, -- 点击用户 ID(游客为 0)
ip varchar(45), -- 访客 IP(支持 IPv6)
create_time int unsigned, -- 时间戳
PRIMARY KEY (id),
KEY domain (domain), -- 按域名查询/聚合统计
KEY create_time (create_time) -- 按时间清理/统计
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Hook 注册点(7 个)
Hook 文件 | 注入位置 | 作用 |
|---|---|---|
|
| 将 |
|
| 帖子格式化后改写外链 |
| 前台路由 switch | 注册 |
| 路由表 | 注册 |
| 后台路由 switch | 注册 |
|
| 插件启用时注入 CSS |
| 语言包 | 注册 38 条中文语言键 |
URL 编码方案
使用 Base64URL(RFC 4648 §5):
原始 URL: https://www.example.com/path?q=test
编码后: aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vcGF0aD9xPXRlc3Q
中转页: /?outlink.htm?url=aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vcGF0aD9xPXRlc3Q
无
+、/、=等特殊字符,无需urlencode解码时严格白名单字符集
[A-Za-z0-9\-_],拒绝非法输入
九、安装与升级
安装
将
oulin_outlink/目录复制到plugin/下后台 → 插件管理 → 找到「外链跳转」→ 安装
安装后默认已启用,进入设置页配置白名单、倒计时等
升级(v1.0 → v1.1)
替换
plugin/oulin_outlink/下所有文件清空
tmp/目录(触发 Hook 重新编译)旧配置自动兼容(
show_url、noindex、log_days有默认值兜底)
卸载
后台 → 插件管理 → 卸载。自动:
删除
bbs_oulin_outlink_log表清除
setting中的插件设置清除
kv中的节流控制键
十、常见问题
Q: 白名单怎么写? 每行一个域名,不要带协议:
example.com # 精确匹配 + 子域名 *.example.com
*.cdn.com # 通配符:匹配 a.cdn.com、b.cdn.com
# 这是注释,会被忽略
Q: 为什么某些外链没有被改写?
可能原因:目标域名在白名单中 / 与本站域名相同 / 链接是 mailto:、tel: 或相对路径 / 帖子格式化为纯文本没有 <a> 标签。
Q: 倒计时设为 0 会怎样? 中转页仍显示,但不出现倒计时文字,只有「立即跳转」和「返回上一页」按钮。
Q: 插件关闭后已有中转页链接会怎样?
中转页路由仍可访问但 OutlinkService 不加载,会报错。建议在设置中关闭「启用」而非卸载,已改写的帖子需重新编辑触发格式化。
十一、版本历史
版本 | 日期 | 变更 |
|---|---|---|
1.1.0 | 2026-08-09 | 请求级缓存、快速预检、安全加固(协议/CRLF/长度/字符白名单)、SQL 聚合统计、日志自动清理 + 保留天数设置、手动清空日志、noindex 双层保障、UTF-8 截断、static_version 缓存刷新、倒计时按钮 timer 清除 |
1.0.0 | — | 初版:外链改写、中转页、倒计时、域名白名单、跳转日志、后台统计 |
十二、兼容性
项目 | 要求 |
|---|---|
Xiuno BBS X | ≥ 1.1 |
PHP | ≥ 8.0 |
数据库 | MySQL 5.7+ / MariaDB 10.3+(InnoDB, utf8mb4) |
前端 | Bootstrap 5 + Tabler Icons(跟随 XiunoX 主题) |
JS 依赖 | 无 jQuery / Alpine.js,纯原生 JS |
作者:oulin · 适用于 Xiuno BBS X 1.1+
内容由 AI 生成,仅供参考。