hexo支持私有文章加密隐藏
想要把个人笔记和博客整合在一起,需要把个人笔记隐藏起来。但由于hexo是静态网站生成器,因此无法实现校验身份再输出内容。只剩下一个方法,用加密算法把私有文章内容加密起来,而解密的key不包含在生成的静态网站中。目前已有优秀的插件可以使用,例如hexo-blog-encrypt。但是这个插件无法满足我的一些需求,因此重新开发,并梳理了一下流程。
主要方法
- 在网站的
_config.yml
文件夹中设置密码 - 文章根据tag标记(包含private),识别私有文章后,使用设置的密码,用AES算法加密文章内容和标题
- 输出文章列表时,对加密文章做特殊标记。前端展示时,隐藏加密文章,避免出现很多“乱码”文章
- 前端通过按钮或快捷键输入密码后,对文章内容和目录进行解密,并恢复加密文章的展示
具体实现
配置文件_config.yml
encrypt: # blog-encrypt
password: somepassword
1. 文章加密aes256
这里不通过插件实现,而是通过主题目录下的scripts
。这个文件夹下的文件是在编译时候使用的,区别于前端js文件。因此这里可以直接调用node模块。完整的使用方法可以参考官方文档。
这里新建一个encrypt.js
文件,使用hexo.extend.filter.register('after_post_render'
方法,给所有文章做一次映射处理。其中加密使用crypto-js
库,它隐藏了很多AES加密的细节,例如IV是自动生成并且包含在密文中的、密码自动填充到合适的位数。
const CryptoJS = require('crypto-js');
hexo.extend.filter.register('after_post_render', (data) => {
// 如果没有设置密钥则跳过
if (!hexo.config.encrypt || !hexo.config.encrypt.password || !data.tags) {
return data;
}
// 跳过没有包含private这个tag的文章
if (!data.tags.map(tag => tag.name).find(tag => tag === 'private')) {
return data;
}
data.origin = data.content;
data.encrypt = true;
const password = hexo.config.encrypt.password;
const ciphertext = CryptoJS.AES.encrypt(data.content, password).toString();
// 替换content为加密后的内容,同时为了方便前端识别,包裹一个特殊的div
data.content = `<div class="encrypted">${ciphertext}</div>`;
return data;
});
2. 文章标题加密
经过上一步加密,执行hexo clean && hexo build && hexo start
预览,可以发现文章内容变成了一堆加密后的内容,但是标题还是原来的,如果有需要,可以对标题进行加密。修改上一步代码:
// 替换content为加密后的内容,同时为了方便前端识别,包裹一个特殊的div
data.content = `<div class="encrypted">${ciphertext}</div>`;
data.title = CryptoJS.AES.encrypt(data.title, password).toString(); // 增加标题加密
return data;
3. 文章列表隐藏
到这里已经全部实现了加密,但是文章列表处依然会出现这些文章。这个需要根据具体使用的主题来实现隐藏。上面加密的步骤中,我们已经给已加密文章增加了一个encrypt
标记。先在文章列表渲染模板里面做前端标记,如果使用的是ejs:
<a
data-encrypted="<%= post.encrypt %>"
>
<div class="post-title" title="<%=post.title %>">
<% if (post.encrypt) { %>
<div class="encrypted"><%= post.title %></div>
<% } else { %>
<%= post.title %>
<% } %>
</div>
</a>
a[data-encrypted='true'] {
display: none;
}
以上代码给加密文章增加了一些标记,并且默认隐藏
4. RSS
如果开启了RSS,需要过滤此类文章。这里不是很好操作,需要直接修改RSS插件的代码。以hexo-generator-feed为例,需要增加一个已加密的filter,可以fork这个项目后做修改发一个自己的包,可以参考这个commit。
如果嫌麻烦,可以直接替换为我制作的版本:@wangsijie/hexo-generator-feed
5. 输入解密密钥
进入解密环节,解密的前提是拿到密钥,在前端网站可以做一个隐藏的入口用于输入密钥,并且保存在localStorage中。
例如,设置为按下ctrl
+ u
的时候弹出输入框:
document.onkeyup = function (e) {
if (e.ctrlKey && e.which === 85) { // ctrl + u
const password = window.prompt('输入密码');
localStorage.setItem('decrypt-key', password);
// decryptPost();
}
};
上面这个代码放在主题的source
目录下的任意地方并且被html引入即可
6. 文章解密
注意到上面代码里有一个decryptPost()
函数被注释掉了,这里开始实现这个函数。首先是文章内容,通过上面添加的class="encrypted"
识别
const items = document.querySelectorAll('.encrypted');
for (const item of items) {
const decrypted = CryptoJS.AES.decrypt(item.textContent, password).toString(CryptoJS.enc.Utf8);
item.parentNode.innerHTML = decrypted;
}
7. 标题解密
这里的标题指的是浏览器的标题。打开一篇文章内容后,发现浏览器标题也是被加密过的。
解密之前先要知道是否经过了加密,一个很土的方法是不管是否加密,强行进行解密,解密成功的话就证明是已加密的。这个方法过于暴力和不稳定,这里采用另一种方法,即文章渲染的时候就通过变量标记自己是否被加密。
在文章渲染模板,例如article.ejs
中增加
<% if (post.encrypt) { %>
<script>
currentPostEncrypted = true;
</script>
<% } %>
然后前端的decryptPost函数增加:
if (typeof currentPostEncrypted !== 'undefined' && currentPostEncrypted) {
// 注意这里需要修改,这个正则表达式假设的标题格式为:文章标题 | 网站标题
const titleSearch = /([^\s]*)\s/.exec(document.title);
if (titleSearch) {
const text = titleSearch[1];
const decrypted = CryptoJS.AES.decrypt(text, password).toString(CryptoJS.enc.Utf8);
document.title = document.title.replace(text, decrypted);
}
}
8. 目录解密
解密目录和文章内容类似,多了一个显示隐藏内容的步骤,上面的函数做下面修改
const items = document.querySelectorAll('.encrypted');
for (const item of items) {
const decrypted = CryptoJS.AES.decrypt(item.textContent, password).toString(CryptoJS.enc.Utf8);
const navTitle = item.parentNode && item.parentNode.parentNode && item.parentNode.parentNode.parentNode;
item.parentNode.innerHTML = decrypted;
// 如果是文章列表,则把data-encrypted设置为false,显示内容
if (navTitle && navTitle.getAttribute('data-encrypted')) {
navTitle.setAttribute('data-encrypted', false);
}
}
9. 密钥测试(可选)
在前端输入密钥后,没有判断密钥是否正确,直接进行解密,可能会在前端的体验上不是很好。密钥的检测,可以通过预置一个随机字符串,以及其加密后的内容来实现。
在encrypt.js
文件中注入这两个变量
hexo.extend.filter.register('template_locals', function(locals){
if (!hexo.config.encrypt || !hexo.config.encrypt.password) {
return locals;
}
const password = hexo.config.encrypt.password;
const random = String(Math.random());
const encrypted = CryptoJS.AES.encrypt(random, password).toString();
// 增加两个全局变量
locals.encryptTestString = random;
locals.encryptTestStringEncrypted = encrypted;
return locals;
});
在footer.ejs
中把这两个变量写到前端js中
<% if (typeof encryptTestString === 'string') { %>
<script>
var encryptTestString = '<%- encryptTestString %>';
var encryptTestStringEncrypted = '<%- encryptTestStringEncrypted %>';
</script>
<% } %>
然后测试密钥的方法就很简单了,使用用户输入的密钥,对encryptTestStringEncrypted
进行解密,并比较解密后得到的是否和encryptTestString
一致。
在decrypt.js
中增加:
if (CryptoJS.AES.decrypt(encryptTestStringEncrypted, password).toString(CryptoJS.enc.Utf8) !== encryptTestString) {
// 密钥错误,删除,并跳过剩余解密步骤
localStorage.removeItem('decrypt-key');
return;
}
10. 总的decrytPost函数
function decryptPost() {
const password = localStorage.getItem('decrypt-key');
if (!password) {
return;
}
// 测试密钥是否正确
if (CryptoJS.AES.decrypt(encryptTestStringEncrypted, password).toString(CryptoJS.enc.Utf8) !== encryptTestString) {
localStorage.removeItem('decrypt-key');
return;
}
// 解密文章内容和文章标题(.encrypt标记过的内容)
const items = document.querySelectorAll('.encrypted');
for (const item of items) {
const decrypted = CryptoJS.AES.decrypt(item.textContent, password).toString(CryptoJS.enc.Utf8);
const navTitle = item.parentNode && item.parentNode.parentNode && item.parentNode.parentNode.parentNode;
item.parentNode.innerHTML = decrypted;
if (navTitle && navTitle.getAttribute('data-encrypted')) {
navTitle.setAttribute('data-encrypted', false);
navTitle.setAttribute('href', navTitle.getAttribute('data-href'));
}
}
// 当前文章如果被加密,则解密浏览器标题
if (typeof currentPostEncrypted !== 'undefined' && currentPostEncrypted) {
const titleSearch = /([^\s]*)\s/.exec(document.title);
if (titleSearch) {
const text = titleSearch[1];
const decrypted = CryptoJS.AES.decrypt(text, password).toString(CryptoJS.enc.Utf8);
document.title = document.title.replace(text, decrypted);
}
}
}
注意事项
如果使用了ajax加载文章
不少主题为了提升体验,使用ajax加载文章,即点击文章标题后,页面只有局部刷新。这时候需要在获得文章内容后,判断是否需要解密。最简单的方式是,ajax成功加载文章并替换页面内容后,立即执行一次decryptPost()
,同时记得处理浏览器标题(document.title
)
如果有文章计数
如果主题中开启了分类文章计数,则会把加密文章也计算进去,导致不准确。需要自行修改相应的渲染文件,忽略掉post.encrypt
为true
的文章
可以修改加密tag标记
上面的例子中,用是否包含private这个tag来判断一篇文章是否需要加密。具体实现的时候可以自行调整
防止私有文章被搜索引擎读取
文章列表中,我们仅对加密文章做了前端隐藏,这时候浏览器有可能会抓取这些文章(加密后的内容),避免方法是,加密文章不使用直接的href
标签,而是把链接放入data-href
中,这样浏览器就不会认为这是一个链接。而在解密后,把data-href
的内容设置为href
即可恢复操作。
总结
以上实现了私有文章的加密和隐藏。达到的效果是,游客进入只能看到公开文章,并且不会察觉的私有文章,浏览体验上不被破坏。而管理员进入博客后,输入密钥,即可查看私有文章,方便的查询自己的笔记等。
文章原始链接:https://sijie.wang/posts/hexo-encrypt
本站文章除特别声明外,均采用
CC BY-NC-SA 4.0
许可协议,转载请保留原始链接