碎碎念
原本的安全跳转页面糟糕的一塌糊涂,因为当时水平有限,所以只能在别人的基础上修改,导致很多地方都不兼容,比如最简单的fancybox我都没有办法排除,会导致无法点击图片进行放大查看,除此之外无法排除友链页面,也无法排除友情链接的跳转卡片,兼容性也很差,群友想要使用我也没法提供解决方案,很是头疼,所以经过整整一个月的酝酿,我胡汉三又回来啦!此次重构大大简化了代码结构,并解决了前面的问题,为了测试稳定性,我还特意悄悄地上线了六天,好像也没人提出什么bug(也有可能是我的人气太少了呜呜呜),这才正式写出该重制版教程,给予需要的朋友以启发。
功能介绍
- 设置替换白名单:按照揽星给出的建议,可以自定义替换白名单匹配,如友链文章引用,好友引用等无需替换,其他链接替换;
- 设置页面白名单:如仅匹配文章页面的链接;
- 设置元素白名单:如仅匹配
id="article-container"
的内容; - 设置跳转白名单:如知乎等,使用跳转页面,但显示为安全,可以自动跳转;
- 注:该教程理论上适用于全部Hexo架构博客,请按照要求修改代码即可,该教程需要有一定的前端水平,如果有问题可以发到评论区,我会尽量解答。
功能实现
这里我还是使用原廿壴博客提供的跳转页模板,但是相关跳转页逻辑完全重构
页面模板
首先需要在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 { "<": "&lt;", "&": "&amp;", '"': "&quot;", ">": "&gt;", "'": "&#39;", }[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脚本,将能匹配上的链接进行替换,请在自定义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||非文章页面无需替换安全链接');
}
});
这里需要修改的部分主要有:
- 第三行替换白名单,这些网站将默认为安全网站,不会被重定向到安全跳转页面,可以将友链的根域名放到这里,如果在链接中匹配到元素,将不进行替换。
- 第十行元素白名单:填写你想替换的页面的某个部分的ID或者类名,查找方式如下:
- 第十六行元素黑名单:比如fancybox,fancybox是图片点击后放大预览的插件,如果链接替换了的话会导致无法正常放大,显示图片异常,这个应该都一样,确定的方式如下:
- 下面两个执行函数,由于我的页面开了pjax,不需要的删除即可,也可以保留,需要替换其中的posts路径,该路径在随便的文章页链接中就能找到:
此时,功能基本实现了,你的文章页的外链卡片应该已经被替换为了安全链接。
评论区
每个评论系统基本上都会有一个回调函数,比如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
}
})()
去掉加号即为正常缩进,注意第一二行,去掉前面的一格空格,可以看到我加了一些限制条件,和上面同理,这里我就不多说了,有什么问题可以在评论区交流。
最后功能实现。