hexo博客有文章加密功能,搜了一圈没找到astro的类似插件,只好参考How To Blog 02: Astro❤️Password实现一下!

原理

  1. 服务端加密: 在构建时使用用户提供的密码加密文章内容
  2. 客户端解密: 用户输入密码后在浏览器中解密内容
  3. 内容渲染: 解密后动态渲染文章内容

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实现一下前端,完美!

文章加密

Astro-文章加密功能实现

作者

桥边红药

发布日期

2025 - 08 - 12