Hexo 博客文章加密指定内容

Hexo 博客文章加密指定内容

加密插件 hexo-blog-encrypt 加密单篇文章非常好用,使用教程在作者的仓库也有明确说明。当时当我们需要加密一篇文章中的某些特定文字、特定段落时,此插件默认的功能无法实现

经过一番折腾,终于搞定了利用 hexo-blog-encrypt 插件来加密文章指定内容的功能,现分享给大家

安装必要的插件和库

安装 hexo-blog-encrypt 插件和 crypto-js 库,安装完后检查 package.json 文件 dependencies 字段是否有对应的字段

1
npm install --save hexo-blog-encrypt crypto-js

创建 Hexo 短代码标签

Hexo 根目录创建 scripts/shortcode-encrypt.js 文件

如果没有 scripts 文件夹则新建一个,内容如下:

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
const CryptoJS = require("crypto-js");

hexo.extend.tag.register('encrypt', (args, content) => {
const password = args[0];

// 添加验证前缀,确保解密结果正确
const prefix = "HEXO_ENCRYPT_PREFIX|";
const suffix = "|HEXO_ENCRYPT_SUFFIX";
const contentWithPrefix = prefix + content + suffix;
const encrypted = CryptoJS.AES.encrypt(contentWithPrefix, password).toString();

return `
<div class="encrypted-block"
data-encrypted="${encodeURIComponent(encrypted)}">
<div class="storage-indicator"></div>
<div class="encrypt-input-group">
<input type="password"
placeholder="请输入密码"
class="encrypt-input"
aria-label="加密内容密码">
<button type="button" class="decrypt-btn">🔑 查看内容</button>
</div>
<div class="decrypt-result">
<div class="decrypted-content"></div>
</div>
</div>
`;
}, { ends: true });

Hexo 根目录创建 source/js/encrypt.js 文件

如果 source 下没有 js 文件夹则新建,内容如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
document.addEventListener('DOMContentLoaded', function() {
// 自动解密:检查本地存储的密码
const encryptedBlocks = document.querySelectorAll('.encrypted-block');

encryptedBlocks.forEach(block => {
const storageKey = `hexo-encrypt-${block.dataset.encrypted}`;
const savedData = localStorage.getItem(storageKey);

if (savedData) {
try {
const { password, expire } = JSON.parse(savedData);

// 检查是否过期
if (expire > Date.now()) {
// 自动解密
block.querySelector('.encrypt-input').value = password;
setTimeout(() => handleDecrypt(block), 300);
} else {
// 过期则清除
localStorage.removeItem(storageKey);
}
} catch (e) {
console.error('Failed to parse saved data', e);
localStorage.removeItem(storageKey);
}
}
});

// 事件委托处理解密
document.body.addEventListener('click', function(e) {
if (e.target.classList.contains('decrypt-btn')) {
handleDecrypt(e.target.closest('.encrypted-block'));
}
});

// 添加回车键支持
document.body.addEventListener('keypress', function(e) {
if (e.target.classList.contains('encrypt-input') && e.key === 'Enter') {
handleDecrypt(e.target.closest('.encrypted-block'));
}
});

// 在指定位置显示提示
function showHint(block, message, type = 'info') {
const hint = document.createElement('div');
hint.className = `auto-decrypt-hint ${type}-hint`;
hint.innerHTML = message;
const inputGroup = block.querySelector('.encrypt-input-group');
inputGroup.appendChild(hint);
}

// 清除提示
function clearDecryptHints(block) {
const hints = block.querySelectorAll('.auto-decrypt-hint');
hints.forEach(hint => hint.remove());
}

function handleDecrypt(block) {
if (!block) return;

const encrypted = decodeURIComponent(block.dataset.encrypted);
const input = block.querySelector('.encrypt-input').value;
const resultArea = block.querySelector('.decrypted-content');
const decryptResult = block.querySelector('.decrypt-result');

// 确保结果区域可见
decryptResult.style.display = 'block';
// 清除所有提示信息
clearDecryptHints(block);

if (!input) {
showHint(block, '⚠️ 请输入密码', 'error');
return;
}

try {
const bytes = CryptoJS.AES.decrypt(encrypted, input);
const text = bytes.toString(CryptoJS.enc.Utf8);

// 使用统一标识符验证
const prefix = "HEXO_ENCRYPT_PREFIX|";
const suffix = "|HEXO_ENCRYPT_SUFFIX";

if (!text || text.indexOf(prefix) === -1 || text.indexOf(suffix) === -1) {
throw new Error('解密失败');
}

// 提取实际内容
const startIndex = text.indexOf(prefix) + prefix.length;
const endIndex = text.indexOf(suffix);
const actualContent = text.substring(startIndex, endIndex);

// 使用 marked.parse() 渲染 Markdown
resultArea.innerHTML = DOMPurify.sanitize(marked.parse(actualContent));
block.classList.add('decrypted');

// 存储密码到本地(有效期7天)
const storageKey = `hexo-encrypt-${block.dataset.encrypted}`;
const expireTime = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天

localStorage.setItem(storageKey, JSON.stringify({
password: input,
expire: expireTime
}));

// 添加自动解密提示
showHint(block, '✔️ 密码正确!7 天内自动解密', 'success');

} catch (error) {
showHint(block, '❌ 密码错误!请重试', 'error');
}
}
});

Hexo 根目录创建 source/css/encrypt.css 文件

如果 source 下没有 css 文件夹则新建,内容如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/* ===== 加密块整体样式 ===== */
.encrypted-block {
border-radius: 8px;
overflow: hidden;
margin: 20px 0;
background: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}

/* ===== 输入区域样式 ===== */
.encrypt-input-group {
display: flex;
padding: 12px 16px;
background: var(--card-bg);
border-bottom: 1px solid var(--card-border);
align-items: center;
}

.encrypt-input {
flex: 0 0 150px;
padding: 10px 16px;
border: 1px solid #E3E8F7;
border-right: none;
border-radius: 6px 0 0 6px;
font-size: 16px;
background: #F7F7F9;
color: var(--text-color);
transition: all 0.3s ease;
}

.encrypt-input:focus {
border-color: var(--theme-color);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--theme-color-rgb), 0.1);
}

.decrypt-btn {
flex: 0 0 auto;
padding: 10px 16px;
background: #425AEF;
border: 1px solid #425AEF;
color: var(--btn-color);
border-left: none;
border-radius: 0 6px 6px 0;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
}

/* 解密提示 */
.auto-decrypt-hint {
flex: 0 0 auto;
display: none;
border-left: 4px solid;
padding: 6px 16px;
margin-left: 15px;
border-radius: 6px;
font-size: 14px;
display: flex;
height: fit-content;
}

/* 移动端适配 */
@media (max-width: 768px) {
.encrypt-input-group {
flex-wrap: wrap;
gap: 8px;
align-items: stretch;
}

.encrypt-input {
border-right: 1px solid #E3E8F7; /* 恢复右侧边框 */
border-radius: 6px !important; /* 四角圆角 */
flex: 0 0 auto !important;
}

.decrypt-btn {
border-left: 1px solid #425AEF; /* 恢复左侧边框 */
border-radius: 6px !important; /* 四角圆角 */
flex: 0 0 auto !important;
}

.auto-decrypt-hint {
width: 100%; /* 占满整行 */
border-left: 4px solid;
margin-left: 0;
margin-top: 8px;
order: 1; /* 强制换到第二行 */
}
}

@media (max-width: 400px) {
.encrypt-input,
.decrypt-btn {
width: 100%;
}
.decrypt-btn {
margin-top: 8px;
height: 44px;
}
}

/* 成功状态 */
.auto-decrypt-hint.success-hint {
background: rgba(46, 204, 113, 0.1);
border-left-color: #2ECC71;
color: #27AE60;
}

/* 错误状态 */
.auto-decrypt-hint.error-hint {
background: rgba(231, 76, 60, 0.1);
border-left-color: #E74C3C;
color: #C0392B;
}

/* 信息状态 */
.auto-decrypt-hint.info-hint {
background: rgba(66, 90, 239, 0.1);
border-left-color: #425AEF;
color: #425AEF;
}

/* ===== 解密结果区域 ===== */
.decrypt-result {
display: none;
padding: 0;
margin-bottom: 12px;
}

.encrypted-block.decrypted .decrypt-result {
display: block;
padding: 10px 20px;
animation: fadeIn 0.5s ease;
}

@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.decrypted-content {
animation: slideIn 0.5s ease 0.1s forwards;
opacity: 0;
transform: translateY(20px);
}

@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}

/* ===== 解密内容Markdown样式 ===== */
.decrypted-content * {
color: var(--text-color) !important;
margin-bottom: 10px;
}

.decrypted-content p {
line-height: 1.8;
margin-bottom: 5px;
font-size: 16px;
}

.decrypted-content ul,
.decrypted-content ol {
padding-left: 28px;
margin-bottom: 5px;
}

.decrypted-content li {
margin-bottom: 5px;
font-size: 16px;
}

.decrypted-content a {
color: var(--theme-color) !important;
text-decoration: none;
border-bottom: 1px solid rgba(var(--theme-color-rgb), 0.3);
transition: all 0.2s ease;
}

.decrypted-content a:hover {
color: var(--theme-hover-color) !important;
border-bottom-color: var(--theme-hover-color);
}

.decrypted-content strong {
font-weight: 600;
}

.decrypted-content em {
font-style: italic;
}

.decrypted-content code {
background: var(--inlinecode-bg);
color: var(--inlinecode-color);
padding: 3px 6px;
border-radius: 4px;
font-size: 15px;
font-family: var(--code-font);
}

.decrypted-content pre {
background: var(--pre-bg);
padding: 20px;
border-radius: 8px;
overflow: auto;
margin-bottom: 5px;
border: 1px solid var(--pre-border);
}

.decrypted-content pre code {
background: none;
color: var(--pre-color);
padding: 0;
border-radius: 0;
font-size: 15px;
}

.decrypted-content blockquote {
border-left: 4px solid var(--blockquote-color);
background: var(--blockquote-bg);
padding: 16px 20px;
margin: 0 0 5px;
border-radius: 0 8px 8px 0;
font-style: italic;
}

.decrypted-content h1,
.decrypted-content h2,
.decrypted-content h3,
.decrypted-content h4,
.decrypted-content h5,
.decrypted-content h6 {
margin-top: 28px;
margin-bottom: 16px;
color: var(--heading-color);
font-weight: 600;
}

.decrypted-content h1 {
font-size: 28px;
border-bottom: 1px solid var(--heading-border);
padding-bottom: 10px;
}

.decrypted-content h2 {
font-size: 24px;
border-bottom: 1px solid var(--heading-border);
padding-bottom: 8px;
}

.decrypted-content h3 {
font-size: 20px;
}

.decrypted-content h4 {
font-size: 18px;
}

.decrypted-content img {
max-width: 100%;
border-radius: 8px;
margin: 16px 0;
display: block;
}

/* 暗黑模式适配 */
[data-theme="dark"] .encrypted-block {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] .encrypt-input {
background: #21232A;
color: #B8B8B8;
border: 1px solid #77787A;
}
[data-theme="dark"] .decrypt-btn {
background: #F2B94B;
color: #21232A;
border: 1px solid #F2B94B;
}

修改安知鱼主题配置文件 _config.anzhiyu.yml

inject: 字段的 head: 字段下引入上述 encrypt.css 文件

1
- <link rel="stylesheet" href="/css/encrypt.css">

inject: 字段的 bottom: 字段下引入以下 js 文件

1
2
3
4
- <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.5/purify.min.js"></script>
- <script src="/js/encrypt.js"></script>

OK,大功告成,现在来测试一下

输入密码 333 查看加密内容:

如何使用 Hexo 短代码加密

将要加密的内容用按以下格式包裹,其中 333 就是你设定的密码:

1
2
3
4
5
{% encrypt "333" %}  
**这是加密内容**
- 输入密码后回车即可
- 7 天内再次访问本页面无需密码
{% endencrypt %}

#建站 #博客 #加密