文章重要更新 240804

  • 新增 Waline 评论区中间页的实现
  • 加入 @清羽飞扬 的插件方案

原文章发表于 240223

本文已收录至 Butterfly 主题美化教程

今年年初站长收到了 wxb 的温暖来电😨…自此以后对博客安全额外关注。好巧不巧那时在 友链朋友圈 刷到类似文章,于是立刻开展学习并全站自查外链安全性。

本文主要构成:

  • Waline 评论区中间页的实现(新增)
    • 方案 1:官方插件实现。但是有点小问题,建议直接看第二个方案。
    • 方案 2:使用博主自写插件,流畅配合 Hexo 中其他中间页插件,同时支持昵称中的外链插入中间页
  • Hexo 博客外链跳转中间页的实现:
    • 方案 1: @清羽飞扬 的插件方案。
    • 方案 2【过期方案】:博主原本的过渡方案。

Waline 评论区实现外链跳转中间页

温馨提示:本章节介绍的内容适用于基于 Vercel 服务端部署的 Waline

当然,如果 Waline 是其他部署方式,那么安装插件的方法其实大差不差,可继续阅读参考。

Waline 评论区独立于 Hexo 博客,是一个另外的系统,因此 Hexo 的各种渲染插件管理不到 Waline 的内容。但是我们可以通过安装 Waline 评论区的插件实现评论中的外链拦截。

这里介绍两种方案:

  • 方案 1:使用官方插件。但是中间页链接无法定制,不够灵活,且存在小 Bug。
  • 方案 2:使用博主本人基于官方插件二次改造的插件。其特点是黑白名单逻辑优化,可定制中间页链接,昵称外链匹配,并修复一些原官方插件中的 Bug。

想立刻上手可以直接看方案 2!

虽然 Waline 的插件不多,但是 Waline 维护者 lizheming 还是写了个相关的插件:plugins/packages/link-interceptor at master · walinejs/plugins (github.com)

本方案将介绍一种繁琐的但是又利于管理的安装 waline 插件的方法,以理解整个插件安装流程。在后续方案 2 中会介绍另一种十分轻松的偷懒方法,简化整个 waline 插件安装过程。

安装

首先从 GitHub 中 git clone 你的 Waline 后端到本地,然后进入你的 Waline 后端项目,输入以下命令安装 npm 包:

1
npm i @waline-plugins/link-interceptor

按照官方 Demo 的做法,在你的 Waline 后端项目的 index.js 中添加对应代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Application = require('@waline/vercel');
const LinkInterceptor = require('@waline-plugins/link-interceptor'); // 增加这一条

module.exports = Application({
// ...这里省略你以前写的东西

// 插件
plugins: [
LinkInterceptor({
whiteList: ['uuanqin.top'], // 白名单
blackList: [], // 黑名单
// interceptorTemplate: `redirect to __URL__ ` // 这是中间页的模板,__URL__会被替换为匹配到的url
})
]
});

interceptorTemplate 选项不写的话,官方默认的 HTML 网页模板为:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Redirect to third party website</title>
</head>
<body data-url="__URL__">
<p>Redirecting to __URL__</p>
<script>location.href = document.body.getAttribute('data-url');</script>
</body>
</html>

部署与测试

将下面的文件加到 .gitignore 文件中:

  • 刚才安装插件新增的 node_modules 文件夹
  • 新增的 package-lock.json

然后 git add 以下文件(也就是说只有以下两个文件发生了改动):

  • 你刚刚改动的 index.js
  • 下载插件后自动变化的 package.json

最后 git commit 并上传 git push。上传成功后 Vercel 会自动重新部署后端。

你可以发表评论进行测试。比如 blackList 中包含 jd.com,那么评论内容出现相关域名链接时将替换链接为插件指定的中间页地址,并展示指定模板。

效果如下(使用默认模板):

image.png

上面的默认模板显示后,紧接着自动跳转到目标链接。

官方插件中存在的问题

黑白名单判断逻辑

该插件的黑白名单判断逻辑:

1
2
3
4
function isValidUrl(url) {
// ...
return !inBlackList || inWhiteList;
}

也就是说它是黑名单优先的:

  • 如果网址不在黑名单,那就立马通过验证。
  • 只有在黑名单且不在白名单才进行中间页拦截。

此外,没有域名通配符逻辑。这样的效果似乎不是很好。

中间页模板(已修复)

此问题当天 PR 当天就修复,赞赞赞!

另外,官方插件中的模板功能并不生效。因为官方插件的函数 outputHtml 写错位置了(已提 PR ,目前成功合并)。

昵称中的外链

官方插件并不过滤昵称中的外链。

不支持带有 # 符号的外链识别

看了下代码,感觉是偷懒…因为它编写的正则表达式有意的排除了带 # 的链接,简化后续域名判断。但是修复这个问题其实并不难(或者存在我没考虑的的情况?),暂且理解为官方为了省事暂时不变写这段逻辑。

针对官方插件中存在的不足,我对官方插件的逻辑进行改进:

  • 修复官方插件中的中间页模板不生效的问题(官方已在 PR #13 同步修复);
  • 黑名单、白名单匹配规则更改;
  • 可以自定义跳转的中间页,以联合 Hexo 中的相关插件;
  • 【v0.1.1 新增】除了评论内容中的外链筛选外,可以以同样的方式为头像旁边的昵称中的外链应用中间页。
  • 【v0.1.2 修复】修复原官方插件中,不对带有 # 链接进行处理的错误逻辑

Readme Card

项目地址:uuanqin/waline-link-interceptor: A plugin of Waline Comment System which can add a intercept page for external links in comments. (github.com)

直接改动 GitHub 仓库完成插件的安装和使用

上一小节在介绍方案 1 时,安装 waline 插件时又要 git clonenpm install 的,可能会特别麻烦。这里介绍直接在 GitHub 仓库改文件的方法,因为两者的本质是一样的。

进入你 GitHub 中的 Waline 后端项目,修改 package.json 文件,添加依赖:

1
2
3
4
5
6
7
8
9
10
{
"name": "template",
"version": "0.0.1",
"private": true,
"dependencies": {
- "@waline/vercel": "latest"
+ "@waline/vercel": "latest",
+ "waline-link-interceptor": "^0.1.2"
}
}

一定要注意:

  • 由于 Json 严格的语法规范,请留意逗号不要多加
  • waline-link-interceptor 后面接版本号,目前的版本为:img

编辑 Waline 后端项目的 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Application = require('@waline/vercel');
const LinkInterceptor = require('waline-link-interceptor'); // 添加这一行

module.exports = Application({
// 插件
plugins: [
LinkInterceptor({
whiteList: [
'uuanqin.top'
],
// blackList: [],
// interceptorTemplate: `hello __URL__ `, // 这是中间页的模板(当不指定跳转链接时默认使用模板)
})
]
});

这个插件的最基本的用法和上面方案 1 中官方插件的用法完全一样。不同的是,黑白名单的逻辑稍有变更:

  • 如果只填写白名单,那么不在白名单内的域名及其子域名,均将重定向至拦截页;
  • 如果只填写黑名单,那么黑名单中的域名及其子域名,均将重定向至拦截页。其余链接放行;
  • 如果黑白名单都填写,那么只拦截在黑名单中但不在白名单中的域名及其子域名。

将这些修改 commit 并 push 到远程仓库的 main 分支后之后,Waline 会自动部署,部署完成后即可看到效果。

关于 Vercel 中的 Production 分支详见官方文档:Deploying Git Repositories with Vercel

进阶使用 - 重定向至指定的链接

有时候,我们并不想使用 Waline 默认提供的跳转页模板(可以说官方这个模板十分简陋了),又或者不想自己手动通过 interceptorTemplate 字段写中间页模板。如果我们已经有了现成的中间页网址,可不可以让外链跳转到指定的地址呢?答案是肯定的,这个插件提供修改中间页链接的可选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = Application({
// 插件
plugins: [
LinkInterceptor({
whiteList: [
'uuanqin.top'
],
// blackList: [],
// interceptorTemplate: `hello __URL__ `, // 如果下面自定义了跳转地址,那么此处模板不生效
redirectUrl: "https://example.com/go.html", // 填写中间页的具体 html 地址。
encodeFunc: (url) =>{
return url; // 填入一个外链 url 的处理函数
}
})
]
});

示例 1:简单示例

免责声明:以下内容仅供学习参考,请读者自行判断合法性并承担风险。

比如我想让中间页地址形式为:

1
https://example.com/go.html?u=https://external-link.com

其中,example.com 表示主站地址,external-link.com 表示外部站点地址。那么, waline-link-interceptor 插件参数填写如下:

1
2
3
4
redirectUrl: `https://example.com/go.html`,
encodeFunc: (url) =>{
return "u="+url;
}

也就是说,外链地址中问号 ? 的前面归 redirectUrl 管,问号后面的归 encodeFunc 管。

比如,像知乎中间页链接就是这种简单的形式(来自评论区小伙伴提供的思路,这种方式有点说不出的妙🤭):

1
https://link.zhihu.com/?target=https://uuanqin.top/

对应的 waline-link-interceptor 配置为:

1
2
3
4
redirectUrl: `https://link.zhihu.com/`,
encodeFunc: (url) =>{
return "target="+url;
}

示例 2:配合 Hexo 中间页插件 hexo-safego

如果你装了后文提到 Hexo 插件 hexo-safego,且假设你的 hexo-safego 的相关配置(在 _config.yml 中)为:

1
2
3
4
5
6
7
# see https://blog.qyliu.top/posts/1dfd1f41/
hexo_safego:
enable: true # 是否启用 hexo-safego 插件
enable_base64_encode: true # 是否启用 Base64 编码链接
enable_target_blank: true # 是否在跳转链接中添加 target="_blank"
url_param_name: 'u' # URL 参数名,用于生成跳转链接
html_file_name: 'go.html' # 跳转页面文件名

也就是说其中间页地址形式为:

1
2
https://example.com/go.html?u=xxxxxxxxxxxxxxxxx  
# 这里表示 `xxxxxxxxxxxxxxxxx` 表示经过 Base64 编码后的外部链接。

那么你在插件 waline-link-interceptor 的配置中可以这样填写:

1
2
3
4
5
redirectUrl: "https://example.com/go.html",
encodeFunc: (url) =>{
// 启用 Base64 编码链接
return "u="+Buffer.from(url).toString('base64') // 或者 return "u="+btoa(url)
}

这样就能 Waline 评论区 + Hexo 文章统一的中间页跳转啦🥳!

Hexo 博客文章或页面的外链跳转

方案一:直接使用跳转插件 hexo-safego

【博主推荐】这是本站目前使用的 Hexo 文章外链跳转方案!

详见友链用户 @清羽飞扬 的文章:安全跳转页面·插件版

效果如下:

image.png

本方案过期提示

  • 本小节为今年(2024)年初事态紧急时探索的方案;
  • 作为临时过渡措施,本方案已经完成了它的使命;
  • 虽然整体的实现并不优雅,但直至目前该方案仍可继续正常工作。

本小节提供一个在 Hexo 实现的简单的方法,涉及到对插件源代码的修改(不优雅),但过渡时期可暂时容忍这一不足。我觉得整体思路是有参考性的。

插件安装与调整

插件的原理是识别出所有的外部链接,并修改其 URL 的格式使之成为内部链接。URL 指向一个中间页(没错,就是我以前嗤之以鼻的「互联网壁垒」)。你可以选择其他方式完成这件事情(比如 Hugo 方案参考友链用户 @大大的蜗牛 的文章: Hugo 外部链接跳转提示页面

插件 hvnobug/hexo-external-link (github.com) 所做的就是这件事情。但是唯一缺点就是最终转换的链接不可定制,于是我就手动修改了一下源代码——即修改博客目录下 blogs\node_modules\hexo-filter-links\lib\filter.js 文件:

1
2
- NewhrefStr = 'href="' + config.url + '/go/#' + NewhrefStr + '"';
+ NewhrefStr = 'href="' + '/pages/go.html#' + NewhrefStr + '"';

这样链接就变成了我想要的样子:/pages/go.html#+ 编码为 Base64 的外部链接地址。

建立中间页

从上面转换后的中间连接可以知道,我设置的中间页位置为 /pages/go.html。其实我只是在 sourse/pages 目录下新建了 go.md 文件而已,Hexo 会自动渲染成 HTML。我这样做只是为了和某些页面统一而已。你也可以自己另外设计独立的页面。

go.md 基本内容如下(省略了样式代码,保留主要的 HTML,看思路即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
---
title:
date: 2024-02-22 00:00:00
aside: false
top_img:
---

<link rel="stylesheet" href="/css/simple_page.css">

<body>
<meta charset="utf-8"/>
<title>GO Page</title>
<div>
<p class="h3">您即将在 &nbsp; <span id="go_time">inf</span>s &nbsp;后跳转到以下地址</p>
<p class="to_address text_align">
</p>
<a href="" class="to_url button" style="color: #FFFFFF;">立即跳转</a>
</div>
</body>

<script>
// 链接解析与设置
const params = window.location.hash;
const encodedTarget = params.slice(1);
const target = atob(encodedTarget); // 对应插件的编解码方法

if (target) {
console.log('解析到的target',target,params,encodedTarget);
document.getElementsByClassName('to_url button')[0].href = target;
document.getElementsByClassName('to_address text_align')[0].textContent = '' + target; // 直接显示目标地址
} else {
console.error('外部链接跳转未指定重定向目标。');
}

// 倒计时逻辑
var second=5;
var time = document.getElementById("go_time");
function show() {

if(second==0){
//跳转页面
location.href=target;
}else{
second--; // 放在if的分支里可以避免出现负数的情况
}
//用来动态设置里面的内容
time.innerHTML=second+"";

}
//用来实现这个一秒实现一次这个方法
setInterval(show,1000);
</script>

不太建议全站启用外链中间页,只对文章内容启用中间页即可。

后记

附一份常用白名单,涵盖程序员常用外链(云服务平台、博客网站、笔记网站等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
allowList:  
- 'github.com'
- 'gitee.com'
- 'tencent.com'
- 'aliyun.com'
- 'csdn.net'
- 'juejin.cn'
- 'jianshu.com'
- 'cnblogs.com'
- 'zhihu.com'
- 'bilibili.com'
- 'feishu.cn'
- 'yuque.com'
- 'kdocs.cn'

如果平时不喜欢各大网站中间页可以考虑安装这个脚本:Open the F**king URL Right Now (greasyfork.org)。或者尝试安装这个插件(我没试过):Skip Redirect (google.com)

Open the F**king URL Right Now

很多时候,在网上读到一篇好文章,我们被作者思想的深度,逻辑论证的缜密,内容的旁征博引所折服,这时我们往往也想去作者给出的相关链接上看看是怎样的奇文给了作者启发,写出了这样一篇振聋发聩的好文,于是点击链接,但是很多时候会突然跳出来一行字,要用户手动复制该链接贴到地址栏访问才行,被网站这样一折腾,刚刚提起来的求知欲一下子烟消云散了。

一次两次也倒罢了,但最近此风甚长,还有愈演愈烈之势,好好的超链接变成了一个个的『超不链接』,其他网站为了留存用户,延长用户停留时间,不惜以暴制暴,进行恶性竞争,结果就是互联网的用户体验变成了各大公司利益的牺牲品,在夹缝中艰难地生存,昔日的信息高速公路上就这样被人为地挖了许多坑。

为了把互联网体验变回我们熟悉的样子,为了让超链接能痛快地把用户送到终点,就写了这样一个简单的脚本…

——OldPanda ,摘自中间页跳转脚本 Open the F**king URL Right Now 的简介

安全跳转必要性

有朋友问,为何要增设这样一个看似多余的步骤?“直接浏览不更直接吗?何必增添一个看似拖沓的跳转环节?”还有人,将其误解为纯粹的技术冗余,误以为这是效仿某些平台所设置的“用户不便”之举…Hexo-SafeGo 插件的核心目的,并非直接进行安全检测,而是作为一种自我保护机制,默默守护着网站的声誉与访问者的信任。现在的插件并没有能力能够主动扫描并消除网络中的所有威胁,但却能有效避免自身网站因缺少必要的安全协议而被浏览器标记为“不安全”,这一小步跳跃,实则是维护网站形象与信誉的一大步。

此插件的实施,更像是一份无形的“免责声明”,它向访问者无声宣告:作为网站管理者,我已经采取措施确保连接的安全性,即便遭遇外部链接可能带来的不确定性,也已事先提醒,尽到了告知的责任。在这个信息错综复杂的时代,这样的透明度与责任感显得尤为重要。

—— @清羽飞扬,摘自中间页插件 hexo-safego 的介绍文章

在编写 waline 插件时,真的如同盲人摸象,全靠猜测和 log 输出,好在需要修改的地方不复杂。

本文经历了过期又翻新的过程。讯飞星火参与正则代码编写,所以这种苦力活最适合交给 AI 做。

最后建议各位博主不要过度依赖各类插件,定期清理文章或评论区的可疑链接才是治本措施。

本文参考