Astro-文章加密功能实现
hexo博客有文章加密功能,搜了一圈没找到astro的类似插件,只好参考How To Blog 02: Astro❤️Password实现一下!
原理
- 服务端加密: 在构建时使用用户提供的密码加密文章内容
- 客户端解密: 用户输入密码后在浏览器中解密内容
- 内容渲染: 解密后动态渲染文章内容
1.在文章frontmatter添加密码字段
在 src/content.config.ts
中添加密码字段:
const post = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
published: z.date(),
description: z.string().optional(),
image: z.string().optional(),
tags: z.array(z.string()).default([]),
category: z.string().optional(),
draft: z.boolean().default(false),
+ password: z.coerce.string().optional(),
}),
});
z.coerce
表示将输入内容强制转换为String
类型,所以密码可以是数字或字母
2. 加密工具实现
创建 src/utils/encrypt.ts
:
// 加密函数
export async function encrypt(text: string, password: string): Promise<string> {
const key = password.length >= 16 ? password.slice(0, 16) : password.padEnd(16, '0');
const iv = crypto.getRandomValues(new Uint8Array(16));
const keyBuffer = new TextEncoder().encode(key);
const textBuffer = new TextEncoder().encode(text);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC', length: 128 },
false,
['encrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
textBuffer
);
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return btoa(String.fromCharCode(...combined));
}
// 解密函数
export async function decrypt(encryptedData: string, password: string): Promise<string> {
const key = password.length >= 16 ? password.slice(0, 16) : password.padEnd(16, '0');
const combinedData = new Uint8Array(
atob(encryptedData).split('').map(char => char.charCodeAt(0))
);
const iv = combinedData.slice(0, 16);
const encrypted = combinedData.slice(16);
const keyBuffer = new TextEncoder().encode(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-CBC', length: 128 },
false,
['decrypt']
);
const decryptedData = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
encrypted
);
return new TextDecoder().decode(decryptedData);
}
3. 密码保护组件
创建 src/components/PasswordProtected.astro
:
---
import { encrypt } from '../utils/encrypt';
interface Props {
password: string;
}
const { password } = Astro.props;
const html = await Astro.slots.render('default');
const encryptedHtml = await encrypt(html, password);
---
<meta name="encrypted-content" content={encryptedHtml} />
<div id="password-protected-wrapper">
<div id="password-form" class="password-form">
<div class="password-container">
<div class="lock-icon">🔒</div>
<p>此文章已加密</p>
<p>请输入密码查看内容</p>
<div class="input-group">
<input
id="password-input"
type="password"
placeholder="请输入密码"
class="password-input"
autocomplete="off"
autofocus
/>
<button id="decrypt-btn" class="decrypt-btn">解锁</button>
</div>
<div id="error-message" class="error-message" style="display: none;"></div>
</div>
</div>
<div id="decrypted-content" class="decrypted-content" style="display: none;">
<!-- 解密后的内容将在这里显示 -->
</div>
</div>
<script>
// 客户端解密和事件处理逻辑
// ... (详细代码见实际文件)
</script>
4. 文章布局集成
修改 src/layouts/PostLayout.astro
:
{password ? (
<PasswordProtected password={password}>
<Markdown>
<slot />
</Markdown>
<CopyRight />
<Comments />
</PasswordProtected>
) : (
<>
<Markdown>
<slot />
</Markdown>
<CopyRight />
<Comments />
</>
)}
- 三元运算符
{password ? <PasswordProtected password={password}>xxx</PasswordProtected>:xxx}
文章页中存在密码,使用上一步中的加密组件包围。
问题1: Invalid key length 错误
在构建过程中遇到 Invalid key length
错误,原因是AES-128 算法要求密钥长度必须精确为16字节。原始代码使用 password.padEnd(16, '0')
只能保证最小长度,但当密码超过16位时不会截断,导致密钥长度不符合要求。
解决方案:
-const key = password.padEnd(16, '0');
+const key = password.length >= 16 ? password.slice(0, 16) : password.padEnd(16, '0');
最后,再由万能的AI实现一下前端,完美!

文章加密