惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
博客园 - Franky
J
Java Code Geeks
美团技术团队
WordPress大学
WordPress大学
V
Visual Studio Blog
量子位
Hugging Face - Blog
Hugging Face - Blog
V
V2EX
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
爱范儿
爱范儿
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
The Cloudflare Blog
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
博客园_首页
U
Unit 42
O
OpenAI News
T
The Blog of Author Tim Ferriss
Microsoft Security Blog
Microsoft Security Blog
H
Help Net Security
S
SegmentFault 最新的问题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Cisco Talos Blog
Cisco Talos Blog
T
Tailwind CSS Blog
A
About on SuperTechFans
AWS News Blog
AWS News Blog
Recorded Future
Recorded Future
A
Arctic Wolf
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Tenable Blog
大猫的无限游戏
大猫的无限游戏
I
Intezer
K
Kaspersky official blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Register - Security
The Register - Security
G
Google Developers Blog
雷峰网
雷峰网
V
Vulnerabilities – Threatpost

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
How To Blog 02: Astro❤️Password | Yesterday17's Blog
Yesterday17 · 2024-03-31 · via Yesterday17's Blog

ToC

  • 前言
  • 定义一个密码
  • 加密与解密
  • 套壳,启动!
  • 体验一下?
  • 关于未来

前言

大家好,好久(一会)不见,我是某昨。

最近把博客进行迁移的过程中,个人体感最明显的缺失功能就是密码。对于时不时会逆向一些专有软件的我而言,有些东西是不大适合公开可访问的。Wordpress 的密码功能很好地解决了这个问题,但在静态博客生成工具中我一直没有找到很好的解决方案。

理论上,实现某篇博客文章的密码保护可以有以下两种方式:

  • 一种是在运行时有一个简单的后端服务,负责校验密码、返回文章内容
  • 一种是在构建时对文章进行对称/非对称加密,然后在前端解密

我选择了第二种,因为第二种的部署方式能够 100% 静态化,架构上也相比前者简单不少。

定义一个密码

首先,我们需要定义一个密码。我选择在 frontmatter 区域增加一个 password 字段:

import { SITE } from "@config";

import { defineCollection, z } from "astro:content";

const blog = defineCollection({

type: "content",

schema: ({ image }) =>

z.object({

14 collapsed lines

author: z.string().default(SITE.author),

published_at: z.date(),

modified_at: z.date().optional().nullable(),

title: z.string(),

featured: z.boolean().optional(),

draft: z.boolean().optional(),

tags: z.array(z.string()).default(["others"]),

ogImage: image()

.refine(img => img.width >= 1200 && img.height >= 630, {

message: "OpenGraph image must be at least 1200 X 630 pixels!",

})

.or(z.string())

.optional(),

description: z.string(),

canonicalURL: z.string().optional(),

password: z.string().optional(),

}),

});

export const collections = { blog };

这样就可以在 Markdown 的首部定义密码了,就像这样:

---

author: Yesterday17

published_at: 2024-03-31T16:58:25.000+08:00

#modified_at:

title: Sample Password-protected page (pass=test)

slug: blog-02-password-example

featured: false

draft: false

tags:

- blog

description: It's just an example.

password: test

---

## ToC

加密与解密

要回答加密和解密的问题,我们首先需要理清楚需要解决的问题:

  1. 如何获取待加密的文本?
  2. 如何加密?
  3. 用户如何输入密码?
  4. 如何解密?
  5. 解密后如何展示?

一项项看,首先是加密的文本。在咕老师的帮助下,我们在 Stackoverflow 上发现可以通过 slot.render() 的方式拿到 HTML 文本:

---

const html = await Astro.slots.render("default");

import { Code } from "astro/components";

---

<Fragment set:html={html} />

<Code code={html} lang="html" />

使用的时候只要把希望加密的组件放到这个 Wrapper 的里面就可以了:

---

import Card from "../components/Card.astro";

import StringWrapper from "../components/StringWrapper.astro";

---

<StringWrapper>

<Card title="Test" />

</StringWrapper>

拿到待加密的文本之后,第二个问题是如何加密。我们选择通过 AES-256-CBC 简单加密一下:

export async function encrypt(data: string, key: string): Promise<string> {

key = key.padEnd(16, "0");

const dataBuffer = Buffer.from(data);

const keyBuffer = Buffer.from(key);

const cryptoKey = await crypto.subtle.importKey(

"raw",

keyBuffer,

{ name: "AES-CBC", length: 256 },

false,

["encrypt"]

);

const iv = crypto.getRandomValues(new Uint8Array(16));

const encryptedData = await crypto.subtle.encrypt(

{ name: "AES-CBC", iv },

cryptoKey,

dataBuffer

);

const combinedData = new Uint8Array(iv.length + encryptedData.byteLength);

combinedData.set(iv);

combinedData.set(new Uint8Array(encryptedData), iv.length);

return Buffer.from(combinedData).toString("base64");

}

这里基本都是用了 WebCrypto,只在 Buffer 的部分偷了一下懒x

加密完成之后就是用户界面了,在副驾驶的帮助下搓了个姑且能看的:

13 collapsed lines

---

import { encrypt } from "@utils/encrypt";

export interface Props {

password: string;

}

const html = await Astro.slots.render("default");

const encryptedHtml = await encrypt(html, Astro.props.password);

---

<meta name="encrypted" content={encryptedHtml} />

<div>

<input

id="password"

class="w-auto rounded border border-skin-fill

border-opacity-40 bg-skin-fill p-2 text-skin-base

placeholder:italic placeholder:text-opacity-75

focus:border-skin-accent focus:outline-none"

placeholder="Enter password"

type="text"

autocomplete="off"

autofocus

/>

<button

id="password-btn"

class="bg-skin-full rounded-md

border border-skin-fill border-opacity-50 p-2

text-skin-base

hover:border-skin-accent"

>

Submit

</button>

</div>

再搓一个配套的解密,但这里就不能用 Buffer 了,需要纯浏览器端可用,最后把代码串起来:

36 collapsed lines

---

import { encrypt } from "@utils/encrypt";

export interface Props {

password: string;

}

const html = await Astro.slots.render("default");

const encryptedHtml = await encrypt(html, Astro.props.password);

---

<meta name="encrypted" content={encryptedHtml} />

<div>

<input

id="password"

class="w-auto rounded border border-skin-fill

border-opacity-40 bg-skin-fill p-2 text-skin-base

placeholder:italic placeholder:text-opacity-75

focus:border-skin-accent focus:outline-none"

placeholder="Enter password"

type="text"

autocomplete="off"

autofocus

/>

<button

id="password-btn"

class="bg-skin-full rounded-md

border border-skin-fill border-opacity-50 p-2

text-skin-base

hover:border-skin-accent"

>

Submit

</button>

</div>

<script is:inline data-astro-rerun>

async function decrypt(data, key) {

key = key.padEnd(16, "0");

const decoder = new TextDecoder();

const dataBuffer = new Uint8Array(

atob(data)

.split("")

.map(c => c.charCodeAt(0))

);

const keyBuffer = new TextEncoder().encode(key);

const cryptoKey = await crypto.subtle.importKey(

"raw",

keyBuffer,

{ name: "AES-CBC", length: 256 },

false,

["decrypt"]

);

const iv = dataBuffer.slice(0, 16);

const encryptedData = dataBuffer.slice(16);

const decryptedData = await crypto.subtle.decrypt(

{ name: "AES-CBC", iv },

cryptoKey,

encryptedData

);

return decoder.decode(decryptedData);

}

function prepare() {

const encrypted = document

.querySelector("meta[name=encrypted]")

?.getAttribute("content");

const input = document.getElementById("password");

const btn = document.getElementById("password-btn");

const article = document.querySelector("#article");

btn?.addEventListener("click", async () => {

const password = input.value;

try {

const html = await decrypt(encrypted, password);

article.innerHTML = html;

} catch (e) {

alert("Incorrect password");

}

});

}

prepare();

document.addEventListener("astro:after-swap", prepare);

</script>

这里值得注意的是 [1][2] 的部分。由于 AstroView Transition 机制,不能保证脚本的执行。因此需要通过 event 的方式手动在页面变换的时候加上 Listener

套壳,启动!

完成了加密组件,接下来就要给我们有密码的文章套上这个加密的壳了。我们先弄个简单的 Wrapper,方便我们套:

---

import Encrypt from "./Encrypt.astro";

export interface Props {

password?: string;

}

const password = Astro.props.password;

---

{

!password ? (

<slot />

) : (

<Encrypt password={password}>

<slot />

</Encrypt>

)

}

然后在合适的地方把 PasswordWrapper 塞进去:

<article id="article" role="article" class="prose mx-auto mt-8 max-w-3xl">

<PasswordWrapper password={post.data.password}>

<Content />

</PasswordWrapper>

</article>

就大功告成啦!

体验一下?

最终的成品在这里。你可以尝试输入密码(test),解密后的内容渲染应该是可以正常工作的。

关于未来

最主要的有/无问题已经解决,接下来就是体验优化的部分了。简单列了一下可以优化的点:

  • 目前的密码没有任何缓存,因此每次刷新都需要重新输入一遍。~~这里可能需要 localStorage 帮一下忙,存一下密码(~~最后用了 query
  • 此外,密码加密的文章在文章列表里也应该有一个专门的图标用来标识(比如🔒) 加了把可爱的锁x
  • 以及,解密成功能不能有个动画(?
  • 最后,有没有可能支持一下文章内的局部加密呢,不过这可能就是很远之后的事情了(笑)

嘛,就是这样x