【XIUNOX插件】外链跳转弹窗 vv1.1.0 [复制链接]

一级用户组


🔗 oulin_outlink — 外链安全跳转插件 v1.1.0

Xiuno BBS X 插件 · 作者 oulin 将帖子内容中的外部链接自动替换为安全中转页,显示安全提醒与倒计时后跳转,支持域名白名单直接放行。


一、插件概述

oulin_outlink 是一款为 Xiuno BBS X 论坛设计的外链安全管理插件。当用户在帖子中发布指向外部网站的链接时,插件自动将其替换为站内中转页 URL。访客点击后先进入一个带安全提醒的中间页面,经过可配置的倒计时后跳转目标站。本站域名和自定义白名单域名可直接 302 放行,不经中转页。

解决什么问题?

痛点

解决方案

恶意链接 / 钓鱼网站

中转页显示完整目标 URL,让用户确认后再跳转

搜索引擎权重外泄

rel="noopener noreferrer nofollow" + noindex 双层保障

外链统计困难

每次跳转记录 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 / 时间)
            └── 倒计时结束或点击「立即跳转」→ 跳转目标

三、安全机制

防护层

实现

协议白名单

仅允许 http://https:////(协议相对,自动补 https:);拒绝 javascript:mailto:tel:

CR/LF 注入防护

isSafeRedirectUrl() 检测 \r\n,防止 HTTP 响应拆分攻击

URL 长度限制

编码串 ≤ 4096 字符,解码后 URL ≤ 4096 字符

base64url 字符白名单

decodeUrl()preg_match('/[^A-Za-z0-9\-_]/') 拒绝非 base64url 字符

host 校验

parse_url() 提取 host,空 host 直接拒绝

noindex 双层保障

HTTP 头 X-Robots-Tag: noindex, nofollow + 模板 <meta name="robots">

rel 属性

跳转按钮 rel="noopener noreferrer nofollow",防 window.opener 攻击和权重传递

CSRF

后台设置保存和清空日志均带 CsrfService::check()

XSS

所有用户输入(URL、自定义提示)通过 esc_html() / esc_attr() 转义

参数化查询

日志写入和清理使用 db_exec_prepared(),防 SQL 注入

权限控制

后台仅 $gid == 1(管理员)和 $gid == 2(超版)可访问


四、性能优化(v1.1.0 新增)

优化

效果

请求级静态缓存

getSettings() / getLocalDomains() / getWhitelistDomains() 三个方法同一请求内只执行一次底层调用,后续直接返回内存缓存

快速预检跳过

rewriteOutlinks() 开头 stripos($html, '<a ') 检测——没有 <a 标签的帖子直接返回,不跑正则。论坛绝大多数帖子不含外链,跳过 90%+ 无效调用

协议相对 URL 归一化

//cdn.example.com 统一补 https: 再判断域名,不再遗漏

SQL 端聚合统计

后台热门域名从「取 500 行 PHP 遍历聚合」改为 db_find_group + GROUP BY domain + COUNT(*),一条 SQL 返回 top 10

日志节流清理

过期日志清理每小时最多执行一次(kv_get/set 节流),不随每次点击触发 DELETE

UTF-8 安全截断

显示 URL 超长时用 xn_substr()(底层 mb_substr)截断,不破坏多字节字符


五、后台管理功能

设置项

设置

类型

范围

默认值

说明

启用外链跳转

开关

开/关

✅ 开

总开关,关闭后帖子外链不被改写

倒计时秒数

数字

0~60

5

中转页倒计时,设为 0 则仅显示「立即跳转」按钮

显示目标链接

开关

开/关

✅ 开

是否在中转页显示完整目标 URL

跳转页 noindex

开关

开/关

✅ 开

防搜索引擎收录中转页

自定义提示

文本

任意

显示在中转页的额外提示,留空用默认提醒

域名白名单

多行文本

域名列表

每行一个域名,支持 *.example.com 通配符,# 开头为注释

日志保留天数

数字

0~3650

90

超过天数的日志自动清理,0 = 永不自动清理

统计概览(后台顶部三卡片)

┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐
│  🖱️ 累计跳转  │  │  📅 今日跳转  │  │  📈 热门域名 TOP 10      │
│     1,234     │  │      56      │  │  example.com      120    │
│              │  │              │  │  blog.test.org      89    │
└──────────────┘  └──────────────┘  │  ...                     │
                                      └──────────────────────────┘
  • 累计跳转oulin_outlink_log 表总行数

  • 今日跳转:今日 0 点后的跳转次数

  • 热门域名 Top 10GROUP 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

方法

说明

getSettings()

获取插件设置(请求级缓存)

saveSettings($s)

保存设置 + 刷新内存缓存

getLocalDomains()

获取本站域名列表(从 app_url + HTTP_HOST + localhost,缓存)

getWhitelistDomains($s)

解析用户白名单文本为数组(支持注释和通配符,缓存)

isWhitelisted($host, $list)

判断域名是否在白名单(精确 + *.example.com + 宽松子域名)

isLocalDomain($host, $list)

判断是否本站域名(含子域名)

isSafeRedirectUrl($url)

安全校验:协议 + host + CR/LF + 长度

encodeUrl($url)

base64url 编码(URL 安全字符,无 padding)

decodeUrl($encoded)

base64url 解码(带长度和字符集保护)

rewriteOutlinks($html, $s)

正则改写 HTML 中的外链为中转链接

logClick($url, $domain, $uid)

记录跳转日志 + 节流清理过期日志

getLogRetentionDays($s)

获取日志保留天数(0~3650)

clearAllLogs()

清空全部日志


八、插件架构

目录结构

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 文件

注入位置

作用

model_inc_file.php

model.inc.php 末尾

OutlinkService.php 加入框架自动加载

model_post_format_end.php

post_format() 末尾

帖子格式化后改写外链

index_route_case_end.php

前台路由 switch

注册 case 'outlink'

model_route_table_end.php

路由表

注册 outlink 路由条目

admin_index_route_case_end.php

后台路由 switch

注册 case 'oulin_outlink'

header_link_after.htm

header.inc.htm <head>

插件启用时注入 CSS

lang_zh_cn_bbs.php

语言包

注册 38 条中文语言键

URL 编码方案

使用 Base64URL(RFC 4648 §5):

原始 URL:  https://www.example.com/path?q=test
编码后:    aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vcGF0aD9xPXRlc3Q
中转页:    /?outlink.htm?url=aHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vcGF0aD9xPXRlc3Q
  • +/= 等特殊字符,无需 urlencode

  • 解码时严格白名单字符集 [A-Za-z0-9\-_],拒绝非法输入


九、安装与升级

安装

  1. oulin_outlink/ 目录复制到 plugin/

  2. 后台 → 插件管理 → 找到「外链跳转」→ 安装

  3. 安装后默认已启用,进入设置页配置白名单、倒计时等

升级(v1.0 → v1.1)

  1. 替换 plugin/oulin_outlink/ 下所有文件

  2. 清空 tmp/ 目录(触发 Hook 重新编译)

  3. 旧配置自动兼容(show_urlnoindexlog_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 生成,仅供参考。



外链跳转弹窗 截图

外链跳转弹窗 截图



版本:vv1.1.0
适用版本:1.1
开发者:oulin

请登录后查看此内容
最新回复
  • 晨曦笔记 一级用户组
    外链中转这思路挺实用,nofollow 加 noindex 双层保护对权重维护很到位,好多站其实只做了一半。想问下统计那块,跳转记录能按时间段导出吗?论坛外链一多,想定期看看哪些域名被点得多,好决定要不要加白名单。另外白名单现在是全局放行吧,能不能按用户组来,比如管理员发的直接跳、普通用户的走提醒?
    1小时前

请先登录后再回复 登录