原本的安全跳转页面糟糕的一塌糊涂,因为当时水平有限,所以只能在别人的基础上修改,导致很多地方都不兼容,比如最简单的fancybox我都没有办法排除,会导致无法点击图片进行放大查看,除此之外无法排除友链页面,也无法排除友情链接的跳转卡片,兼容性也很差,群友想要使用我也没法提供解决方案,很是头疼,所以经过整整一个月的酝酿,我胡汉三又回来啦!此次重构大大简化了代码结构,并解决了前面的问题,为了测试稳定性,我还特意悄悄地上线了六天,好像也没人提出什么bug(也有可能是我的人气太少了呜呜呜),这才正式写出该重制版教程,给予需要的朋友以启发。
id="article-container"
的内容;这里我还是使用原廿壴博客提供的跳转页模板,但是相关跳转页逻辑完全重构
首先需要在source文件夹下创建go.html,写入以下内容:
---
layout: false
---
<!DOCTYPE html>
<html data-user-color-scheme="light">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<title>
安全中心 | LiuShen's Blog
</title>
<link rel="icon" class="icon-favicon" href="/" />
<link rel="stylesheet" href="https://lib.baomitu.com/twitter-bootstrap/4.6.1/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://at.alicdn.com/t/font_1736178_lbnruvf0jn.css" />
<style type="text/css">
/* // 向上渐隐显示(主内容使用) */
@-webkit-keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(24px);
}
100% {
opacity: 1;
transform: translateY(-80px);
}
}
@keyframes fadeInUp {
0% {
opacity: 0;
-webkit-transform: translateY(24px);
-ms-transform: translateY(24px);
transform: translateY(24px);
}
100% {
opacity: 1;
-webkit-transform: translateY(-80px);
-ms-transform: translateY(-80px);
transform: translateY(-80px);
}
}
/* // 向上渐隐显示(成功错误提示) */
@-webkit-keyframes alertFadeInUp {
0% {
opacity: 0;
transform: translateY(24px);
}
75% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
}
}
@keyframes alertFadeInUp {
0% {
opacity: 0;
-webkit-transform: translateY(24px);
-ms-transform: translateY(24px);
transform: translateY(24px);
}
75% {
opacity: 1;
-webkit-transform: translateY(0);
-ms-transform: translateY(0);
transform: translateY(0);
}
100% {
opacity: 0;
}
}
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, -350%, 0);
}
}
@keyframes fadeOutUp {
0% {
opacity: 1;
}
to {
opacity: 0;
-webkit-transform: translate3d(0, -350%, 0);
transform: translate3d(0, -350%, 0);
}
}
:root {
--blue: #007bff;
--indigo: #6610f2;
--purple: #6f42c1;
--pink: #e83e8c;
--red: #dc3545;
--orange: #fd7e14;
--yellow: #ffc107;
--green: #28a745;
--teal: #20c997;
--cyan: #17a2b8;
--white: #fff;
--gray: #6c757d;
--gray-dark: #343a40;
--primary: #007bff;
--secondary: #6c757d;
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
--light: #f8f9fa;
--dark: #343a40;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", Roboto, "Helvetica Neue", Arial, "Noto Sans",
"Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
[data-user-color-scheme="dark"] {
--body-bg-color: #22272e;
--board-bg-color: #2b313a;
--text-color: #adbac7;
--sec-text-color: #b3bac1;
--post-text-color: #adbac7;
--post-heading-color: #adbac7;
--post-link-color: #34a3ff;
--link-hover-color: #30a9de;
--link-hover-bg-color: #22272e;
--line-color: #adbac7;
--navbar-bg-color: #22272e;
--navbar-text-color: #cbd4dc;
--subtitle-color: #cbd4dc;
--scrollbar-color: #30a9de;
--scrollbar-hover-color: #34a3ff;
--button-bg-color: transparent;
--button-hover-bg-color: #46647e;
--highlight-bg-color: #2d333b;
--inlinecode-bg-color: rgba(99, 110, 123, 0.4);
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color);
border-radius: 6px;
}
html {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
html,
body {
/* background: #f3f4f5; */
/* font-family: PingFang SC, Hiragino Sans GB, Arial, Microsoft YaHei,
Verdana, Roboto, Noto, Helvetica Neue, sans-serif; */
font-family: var(--font-family-sans-serif);
padding: 0;
margin: 0;
background-color: var(--body-bg-color);
color: var(--text-color);
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
height: 100%;
}
body {
font-size: 1rem;
}
p,
div {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
}
body a:hover {
color: var(--link-hover-color);
text-decoration: none;
}
.go-page {
height: 100%;
}
.content {
/* padding-top: 220px; */
width: 450px;
margin: auto;
word-break: break-all;
height: 100%;
}
.content .logo-img {
margin-bottom: 20px;
text-align: center;
padding-top: 220px;
}
.content .logo-img p:first-child {
font-size: 22px;
}
.content .logo-img img {
display: block;
width: 175px;
height: 48px;
margin: auto;
margin-bottom: 16px;
}
.content .loading-item {
background: #fff;
padding: 24px;
border-radius: 12px;
border: 1px solid #e1e1e1;
margin-bottom: 10px;
}
/* 绿色 */
.content .tip1 {
background: #f0f9ea;
}
/* 黄色 */
.content .tip2 {
background: #fdf5e6;
}
/* 红色 */
.content .tip3 {
background: #fef0f0;
}
.content .icon-snapchat-fill {
font-size: 20px;
color: #fc5531;
border: 1px solid #fc5531;
border-radius: 50%;
width: 32px;
text-align: center;
margin-right: 5px;
}
.content .tip1 .icon-snapchat-fill {
color: var(--post-link-color);
border-color: var(--post-link-color);
}
.content .loading-text {
font-size: 16px;
font-weight: 600;
color: #222226;
line-height: 22px;
/* margin-left: 12px; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content .flex {
display: flex;
align-items: center;
}
.content .flex-end {
display: flex;
justify-content: flex-end;
}
/* #267dcc 蓝色 */
.content .loading-color1 {
color: var(--post-link-color);
}
.content .loading-color2 {
color: #fc5531;
}
.content .loading-tip {
padding: 12px;
margin-bottom: 16px;
border-radius: 4px;
}
.content .loading-topic {
font-size: 14px;
color: #222226;
line-height: 24px;
margin-bottom: 24px;
}
.loading-topic .flex {
flex-direction: column;
}
.content .loading-img {
width: 24px;
height: 24px;
}
/* #fc5531; #fc5531*/
.content .loading-btn {
font-size: 14px;
color: var(--post-link-color);
border: 1px solid var(--post-link-color);
display: inline-block;
box-sizing: border-box;
padding: 6px 18px;
border-radius: 18px;
margin-left: 8px;
}
.content .loading-btn:hover {
color: var(--link-hover-color);
border-color: var(--link-hover-color);
}
.content .loading-btn-github {
width: 121px;
background: #fc5531;
color: #fff;
}
.hidden {
display: none;
}
.form-control.hidden {
display: none !important;
}
.mp-img-box {
text-align: center;
margin-bottom: 10px;
}
.mp-img {
max-width: 400px;
width: 100%;
box-shadow: 5px 5px 15px rgb(0 0 0 / 8%);
margin-bottom: 5px;
}
.fadeInUp {
-webkit-animation-name: fadeInUp;
animation-name: fadeInUp;
}
.alertFadeInUp {
-webkit-animation-name: alertFadeInUp;
animation-name: alertFadeInUp;
-webkit-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.fadeOutUp {
-webkit-animation-name: fadeOutUp;
animation-name: fadeOutUp;
}
.fade-animate {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.go-alert {
margin: 0 auto;
width: 110px;
position: absolute;
left: 46%;
top: 5%;
opacity: 0;
text-align: center;
}
.footer {
text-align: center;
position: relative;
margin-bottom: 20px;
}
.footer a {
color: var(--text-color);
}
.flex-box {
display: flex;
height: 100vh;
flex-direction: column;
}
.flex-contain {
flex: 1;
}
.flex-footer {
height: 24px;
}
@media (max-width: 767.98px) {
.content {
width: 94%;
}
.content .logo-img {
padding-top: 120px;
}
}
</style>
</head>
<body class="web-font">
<div id="goPage" class="go-page">
<div class="alert alert-danger go-alert hidden" role="alert">
验证失败
</div>
<div class="content">
<div class="flex-box">
<div class="flex-contain">
<div class="logo-img">
<p class="blog-name">LiuShen's Blog</p>
<p class="blog-description"></p>
</div>
<!-- 加载ing... -->
<div class="loading-item loading-safe flex">
<i class="iconfont icon-snapchat-fill"></i>
<div class="loading-text">链接安全性检验中 请稍后...</div>
</div>
<div class="go-box"></div>
</div>
<div class="footer flex-footer">
©2021-2024
<a href="https://www.qyliu.top" class="blog-name"><span>LiuShen's Blog</span></a>
版权所有
</div>
</div>
</div>
</div>
<!-- goPage end -->
<script src="https://lib.baomitu.com/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js"></script>
<script type="module">
// 请根据自己博客修改
const config = {
// 标题
title:
"安全中心 | LiuShen's Blog",
// 地址栏图标
iconFavicon: "https://cdn.qyliu.top/i/2024/03/21/65fc56832e37d.png",
// 二维码地址
// mpImgSrc: "/img/wxgzh.webp",
// 博客名称
blogName: "LiuShen's Blog",
// 博客描述
blogDescription: "柳影曳曳,清酒孤灯,扬笔撒墨,心境如霜",
// 白名单
safeUrl: [
// 平台 常用平台不用改哈
"github.com",
"gitee.com",
"csdn.net",
"zhihu.com",
"pan.baidu.com",
"baike.baidu.com",
"hexo.io",
"leancloud.cn",
"nodejs.cn",
"jsdelivr.com",
"ohmyposh.dev",
"nerdfonts.com",
"douban.com",
"waline.js.org",
"developer.mozilla.org",
"qyliu.top",
// 好友博客 增加自己的博客友链
],
tipsTextError: "链接错误,关闭页面返回本站",
// tipsTextDownload:
// "从廿壴(ganxb2)微信公众号获取暗号≖‿≖✧ o‿≖✧(๑•̀ㅂ•́)و✧",
// "(๑•̀ㅂ•́)و✧“博客”微信公众号关注走一波o‿≖✧",
tipsTextDanger: "该网址未在确认的安全范围内",
tipsTextSuccess: "该网址在确认的安全范围内",
textDanger:
"您即将离开博客去往如下网址,请注意您的账号隐私安全和财产安全:",
textSuccess: "您即将离开博客去往如下网址",
// 后续改成leancloud获取(下载验证码)
// wpValidate: "9498",
};
// 获取地址
const getQueryString = (name, type) => {
// 构造一个含有目标参数的正则表达式对象
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"),
regDown = new RegExp("&type=" + type),
// 匹配地址参数
r = window.location.search.substr(1).match(reg),
d = window.location.search.substr(1).match(regDown),
isDownload = false;
// 反编译回原地址 取第3个值,不然就返回 Null
if (r !== null) {
// 如果d不为空,则显示下载提示
if (d !== null) {
isDownload = true;
}
return { url: decodeURIComponent(r[2]), isDownload: isDownload };
}
return null;
};
// xss攻击(绑定值时使用)
const xssCheck = (str, reg) => {
return str
? str.replace(
reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g,
function (a, b) {
if (b) {
return a;
} else {
return {
"<": "<",
"&": "&",
'"': """,
">": ">",
"'": "'",
}[a];
}
}
)
: "";
};
// 其他地址校验白名单
const othersValidate = (config, getLinkUrl) => {
let isSafeUrl = false,
safeUrl = config.safeUrl,
url = xssCheck(getLinkUrl.url);
console.log("shuchuchuchcu", safeUrl)
console.log("shuchuchuchcu", url)
if (safeUrl.length !== 0) {
for (let i = 0; i < safeUrl.length; i++) {
const ele = safeUrl[i];
if (url.includes(ele)
|| url.includes(ele + '/')
|| url.includes('https://' + ele)
|| url.includes('https://' + ele + '/')
|| url.includes('http://' + ele)
|| url.includes('http://' + ele + '/')) {
isSafeUrl = true;
break;
}
}
}
return isSafeUrl;
};
// 模版基础配置初始
const goInit = (config) => {
// $(function () {
const tplConfig = {
loadingType: "loading-error",
tipType: "tip3",
tipsText: config.tipsTextError,
loadingTopicText: config.textDanger,
loadingColorType: "loading-color2",
goUrl: "/",
},
getLinkUrl = getQueryString("goUrl", "goDown"),
loadingSafe = document.querySelector(".loading-safe"),
goBox = document.querySelector(".go-box"),
title = document.querySelector("title"),
iconFavicon = document.querySelector(".icon-favicon"),
blogName = document.querySelectorAll(".blog-name"),
blogDescription = document.querySelector(".blog-description");
// 初始化:标题,favicon,博客名称,博客描述
title.textContent = config.title;
iconFavicon.setAttribute("href", config.iconFavicon);
blogName.forEach((element) => {
element.textContent = config.blogName;
});
blogDescription.textContent = config.blogDescription;
// 根据地址栏参数判断是下载地址还是纯外链,外链则直接修改a标签按钮url,用户点击跳转
if (getLinkUrl) {
// 可参考csdn加入后端请求验证地址是否白名单再进一步给出不同场景状态:是白名单,则绿+蓝,否则黄+红
const isSafeUrl = othersValidate(config, getLinkUrl);
tplConfig.loadingType = "loading-others";
tplConfig.goUrl = xssCheck(getLinkUrl.url);
if (isSafeUrl) {
tplConfig.tipType = "tip1";
tplConfig.tipsText = config.tipsTextSuccess;
tplConfig.loadingTopicText = config.textSuccess;
tplConfig.loadingColorType = "loading-color1";
// 白名单链接直接跳转
setTimeout(() => {
const goUrlBtn = document.querySelector(".go-url-btn");
goUrlBtn.click();
}, 2000);
} else {
tplConfig.tipType = "tip2";
tplConfig.tipsText = config.tipsTextDanger;
tplConfig.loadingTopicText = config.textDanger;
tplConfig.loadingColorType = "loading-color2";
}
}
else {
// 错误
tplConfig.tipType = "tip2";
tplConfig.tipsText = config.tipsTextError;
}
const othersTpl = `
<div class="loading-topic">
<span
>${tplConfig.loadingTopicText}</span
>
<a class="${tplConfig.loadingColorType} go-url">${tplConfig.goUrl}</a>
</div>
<div class="flex-end">
<a rel="noopener external nofollow noreferrer" class="loading-btn go-url-btn" href="${tplConfig.goUrl}" target="_self">继续</a>
</div>
`;
const tpl = `
<div class="loading-item ${tplConfig.loadingType} hidden">
<div class="flex loading-tip ${tplConfig.tipType}">
<i class="iconfont icon-snapchat-fill ${tplConfig.loadingType === "loading-download" && "hidden"
}"></i>
<div class="loading-text">
${tplConfig.tipsText}
</div>
</div>
${tplConfig.loadingType === "loading-others"
? othersTpl
// : tplConfig.loadingType === "loading-download"
// ? downloadTpl
: ""
}
</div>
`;
// tpl渲染
goBox.innerHTML = tpl;
const loadingItem = document.querySelector(".go-box .loading-item");
loadingSafe.classList.add("fadeOutUp", "fade-animate");
loadingItem.classList.remove("hidden");
loadingItem.classList.add("fadeInUp", "fade-animate");
};
goInit(config);
</script>
</body>
</html>
以上代码可能需要修改的部分只有一个地方,白名单,不过这里的白名单都是通用的,可以不进行修改,这里的白名单为跳转白名单,详情请看功能介绍,下面是页面展示:
下面就是我重构的内容,使用JS脚本,将能匹配上的链接进行替换,请在自定义JS代码部分添加以下内容:
function updateLinks() {
// 定义白名单数组
var whitelist = [
'qyliu.top', // 添加您不想替换链接的域名或路径片段
'zouht.com',
'akilar.top',
……
];
var containerArticle = document.getElementById("article-container");
if (containerArticle) {
var links = containerArticle.getElementsByTagName("a");
for (var i = 0; i < links.length; i++) {
var link = links[i];
var hasFancybox = link.hasAttribute("data-fancybox");
var isSafeGo = link.href.startsWith('/go.html');
// 使用 Array.prototype.some() 来检查链接的 href 是否包含白名单中的某个元素
var isWhitelisted = whitelist.some(function(whitelistedItem) {
return link.href.includes(whitelistedItem);
});
// 如果没有特定属性且链接没有安全跳转,且链接不在白名单中
if (!hasFancybox && !isSafeGo && !isWhitelisted) {
var originalUrl = link.href;
link.href = "/go.html?goUrl=" + encodeURIComponent(originalUrl) + "&type=goDown";
}
}
}
}
// 在 PJAX 完成时调用函数
document.addEventListener('pjax:complete', function() {
// 检查当前路径是否以 "/posts/" 开头
if (window.location.pathname.startsWith('/posts/')) {
updateLinks();
console.log('pjax||文章页面,准备替换安全链接');
} else {
console.log('pjax||非文章页面无需替换安全链接');
}
});
// 在页面加载完成后调用函数
window.addEventListener('load', function() {
// 检查当前路径是否以 "/posts/" 开头
if (window.location.pathname.startsWith('/posts/')) {
updateLinks();
console.log('load||文章页面,准备替换安全链接');
} else {
console.log('load||非文章页面无需替换安全链接');
}
});
这里需要修改的部分主要有:
此时,功能基本实现了,你的文章页的外链卡片应该已经被替换为了安全链接。
每个评论系统基本上都会有一个回调函数,比如butterfly主题我们定位到文件:[blogroot]themes\butterfly\layout\includes\third-party\comments\twikoo.pug,修改其中的代码:
- const { envId, region, option } = theme.twikoo
- const { use, lazyload, count } = theme.comments
script.
(() => {
const getCount = () => {
const countELement = document.getElementById('twikoo-count')
if(!countELement) return
twikoo.getCommentsCount({
envId: '!{envId}',
region: '!{region}',
urls: [window.location.pathname],
includeReply: true
}).then(res => {
countELement.textContent = res[0].count
}).catch(err => {
console.error(err)
})
}
const init = () => {
twikoo.init(Object.assign({
el: '#twikoo-wrap',
envId: '!{envId}',
region: '!{region}',
onCommentLoaded: () => {
btf.loadLightbox(document.querySelectorAll('#twikoo .tk-content img:not(.tk-owo-emotion)'))
+ document.querySelectorAll('#twikoo .tk-comments-container a').forEach(function(aEl){
+ if (!aEl.hasAttribute('data-fancybox')) {
+ if (!aEl.href.startsWith(window.location.origin)) {
+ aEl.href = '/go.html?goUrl=' + encodeURIComponent(aEl.href) + '&type=goDown';
+ }
+ }
+ });
}
}, !{JSON.stringify(option)}))
!{count ? 'GLOBAL_CONFIG_SITE.isPost && getCount()' : ''}
}
const loadTwikoo = () => {
if (typeof twikoo === 'object') setTimeout(init,0)
else getScript('!{url_for(theme.asset.twikoo)}').then(init)
}
if ('!{use[0]}' === 'Twikoo' || !!{lazyload}) {
if (!{lazyload}) btf.loadComment(document.getElementById('twikoo-wrap'), loadTwikoo)
else loadTwikoo()
} else {
window.loadOtherComment = loadTwikoo
}
})()
去掉加号即为正常缩进,注意第一二行,去掉前面的一格空格,可以看到我加了一些限制条件,和上面同理,这里我就不多说了,有什么问题可以在评论区交流。
最后功能实现。