PKCS5与PKCS7填充模式辨析:跨平台AES加解密避坑指南
1. 从一次“解密失败”的排查说起为什么填充模式如此关键最近在做一个跨平台的数据交换项目涉及到Java后端和C#客户端之间的AES加密通信。后端用的是JDK 1.8自带的加密库而客户端用的是.NET Framework的System.Security.Cryptography。测试阶段一切顺利但到了生产环境偶尔会出现C#端解密失败抛出“Padding is invalid and cannot be removed”的异常。这个问题时隐时现让人头疼。起初我怀疑是密钥或IV初始化向量传输出了问题或者字符编码不一致。但经过反复核对和日志记录确认这些基础参数都是正确且一致的。最后我把目光锁定在了那个平时很少被关注的参数上填充模式Padding。在Java端我习惯性地设置了PKCS5Padding而在C#端我设置的是PKCS7。一个“5”一个“7”难道不是一回事吗在很多博客和快速入门的示例代码里它们经常被混用甚至直接说“PKCS5Padding就是PKCS7Padding”。但这次的生产环境故障告诉我事情没那么简单。这个看似微小的差异正是导致跨平台加解密失败的元凶之一。今天我们就彻底掰开揉碎聊聊PKCS5 Padding和PKCS7 Padding到底有什么区别以及在实际开发中我们该如何正确选择和使用它们避免踩进我踩过的这个坑。2. 填充的本质为什么加密前需要给数据“加料”在深入区别之前我们必须先理解填充Padding为什么存在。对称加密算法如AES、DES通常是以“块”为单位进行操作的。比如AES它就是一个块加密算法其块大小Block Size是固定的128位16字节。这意味着AES加密器每次只能处理恰好16字节的明文数据。那么问题来了我们的原始数据长度不可能总是16字节的整数倍。比如你要加密一句“Hello World!”长度是12字节不够16字节或者一个35字节的文件也不是16的倍数。这时候直接加密就行不通了。填充就是为了解决这个问题在加密之前将原始数据的末尾填充至块大小的整数倍。这样加密器就能愉快地以完整的块为单位进行处理了。解密后再根据填充规则将添加的填充字节移除恢复原始数据。所以填充模式的核心是一套规则它明确规定了两个事如何填How to Pad当数据长度不是块大小的整数倍时具体添加什么字节添加多少。如何删How to Unpad解密后如何识别并安全地移除这些填充字节而不误伤原始数据。不同的填充模式就是不同的“如何填”和“如何删”的规则。PKCS5和PKCS7就是其中两种非常流行且容易混淆的规则。3. PKCS5 Padding被限定的“元老”我们先来看PKCS5。它的定义来自于RSA实验室的公钥密码学标准Public-Key Cryptography Standards第五号文档简称PKCS#5。这份文档的全称是“Password-Based Encryption Standard”顾名思义它最初主要是为了基于密码的加密方案而设计的。PKCS5 Padding的规则非常清晰填充字节的值每个填充字节的值等于需要填充的字节数。填充字节的数量如果需要填充N个字节才能使数据长度达到块大小的整数倍那么就在末尾添加N个字节每个字节的值都是N。举个例子假设块大小是8字节这是理解PKCS5的关键。我们有一段数据0x48 0x65 0x6c 0x6c 0x6f“Hello”的ASCII共5字节。距离下一个块边界8字节还差3个字节。根据规则我们需要填充3个字节每个字节的值都是0x03。所以填充后的数据为0x48 0x65 0x6c 0x6c 0x6f 0x03 0x03 0x03。解密端看到最后三个字节都是0x03就知道需要移除最后3个字节。这里有一个至关重要的限制也是PKCS5与PKCS7产生历史性区别的根源在PKCS#5标准中明确将块大小限定为8字节64位。这是因为PKCS#5标准制定时1993年DES算法块大小64位/8字节是主流。所以PKCS5 Padding是专门为8字节块加密算法如DES设计的。注意正因为这个历史原因当你使用AES块大小16字节时理论上不应该再使用“PKCS5Padding”这个术语。但在实际中由于历史惯性和API的兼容性它被广泛地“借用”和“扩展”了。4. PKCS7 Padding更通用的“继承者”随着加密算法的发展出现了AES块大小16字节等使用更大块大小的算法。显然PKCS5 Padding的8字节限制变得不合时宜。于是在更广泛的加密语法标准中如RFC 2315: PKCS #7定义了一种填充方式其规则与PKCS5 Padding在数学形式上完全一致但取消了对块大小的限制。PKCS7 Padding的规则填充字节的值每个填充字节的值等于需要填充的字节数。填充字节的数量如果需要填充N个字节才能使数据长度达到块大小的整数倍那么就在末尾添加N个字节每个字节的值都是N。发现了吗它的描述和PKCS5一模一样。唯一的区别就是PKCS7不限定块大小。它可以是8字节DES、16字节AES、或者任何其他块大小。继续上面的例子但块大小变为AES的16字节。数据仍是5字节的“Hello”。距离下一个块边界16字节还差11个字节。根据PKCS7规则我们需要填充11个字节每个字节的值都是0x0B十进制的11。填充后的数据为0x48 0x65 0x6c 0x6c 0x6f 11个0x0B。一个特殊情况如果原始数据长度恰好是块大小的整数倍怎么办例如16字节的数据在AES中刚好是一个完整块。此时按照规则“需要填充N个字节”其中N等于块大小16。这意味着我们会在原始数据后额外填充一个完整的块其16个字节的值都是0x10十进制16。这样做的目的是为了让解密算法能够无歧义地执行删除填充的操作。解密后它会检查最后一个字节的值0x10然后删除最后16个字节得到原始数据。如果没有这个额外填充解密端无法判断最后16个字节是原始数据还是填充数据如果原始数据末尾恰好也是0x10 0x10...。5. 核心辨析历史沿革、算法支持与API实现现在我们可以清晰地总结它们的区别与联系了。1. 历史渊源与标准定义PKCS5 Padding源于PKCS#5标准是为8字节块加密算法如DES专门定义的填充方案。这是它的“官方身份”。PKCS7 Padding源于PKCS#7标准主要定义加密消息语法是一种通用的填充方案适用于任意块大小。可以认为PKCS7是PKCS5在概念上的超集或泛化。2. 适用范围块大小PKCS5理论上仅适用于8字节块大小。PKCS7适用于任意块大小8, 16, 32...字节。3. 数学形式与操作在填充操作本身上当块大小为8字节时PKCS5和PKCS7完全等同。它们填充和移除填充的算法逻辑是一模一样的。因此有人会说“PKCS5就是PKCS7在8字节块下的特例”这句话在数学和操作层面是完全正确的。4. 实际开发中的混乱与兼容性这是最容易让人困惑的地方也是我踩坑的原因。由于PKCS5出现得更早、名字更短许多加密库的API在设计时即使底层支持AES16字节也依然沿用了PKCS5Padding这个名称。此时库开发者实际上是在用PKCS5的名字实现PKCS7的逻辑。Java / JCE (Java Cryptography Extension)在JDK中无论是Cipher.getInstance(AES/CBC/PKCS5Padding)还是Cipher.getInstance(DES/CBC/PKCS5Padding)你使用的都是同一种填充实现。实际上Sun/Oracle的JCE提供者内部PKCS5Padding这个标识符被统一映射到了PKCS7的填充逻辑上。它会根据当前密码算法实际的块大小AES是16DES是8来执行填充。所以在Java里写AES/CBC/PKCS5Padding在技术上是可行的也是普遍做法但你要明白你实际用的是PKCS7。Bouncy Castle这样的第三方加密库则通常会同时提供PKCS5Padding和PKCS7Padding两个明确的选项。.NET / C#在System.Security.Cryptography命名空间下填充模式是通过PaddingMode枚举指定的。其中明确有PaddingMode.PKCS7而没有PKCS5。这更符合标准定义。当你选择PKCS7时它会根据Aes块大小16或DES块大小8自动应用正确的填充。OpenSSL在命令行或C API中通常使用-pkcs7参数来指定PKCS7填充。虽然其历史版本可能有一些别名但现代版本更倾向于使用符合标准的术语。其他语言Python, Node.js等情况类似需要查阅具体库的文档。例如Python的cryptography库通常使用PKCS7这个名称。而一些库可能为了兼容性继续提供PKCS5作为别名。结论性对比表格特性PKCS5 PaddingPKCS7 Padding来源标准PKCS#5 (Password-Based Encryption)PKCS#7 (Cryptographic Message Syntax)设计目标专门用于8字节块密码如DES通用的、适用于任意块大小的填充方案块大小限制严格限定为8字节无限制支持8, 16, 32...字节填充规则填充N个字节每个字节值为N填充N个字节每个字节值为N在8字节块下与PKCS7填充完全相同与PKCS5填充完全相同在现代API中的体现常作为历史别名存在尤其在Java中实际指向PKCS7逻辑。更标准、更通用的名称被.NET、OpenSSL等广泛采用。核心关系是PKCS7在块大小固定为8字节时的一个具体实例。是PKCS5在概念上的泛化超集。6. 实战避坑指南如何确保跨平台加解密成功理解了理论最终要落到实操。如何避免文章开头提到的那个坑以下是我的经验总结。1. 首要原则显式统一而非依赖别名在进行跨系统如Java后端与C#前端、服务端与移动端的加解密交互时不要依赖某个平台对“PKCS5”的别名解释。最安全、最标准的方式是在所有参与方都明确指定并使用“PKCS7”作为填充模式。C#端这很自然因为.NET只有PaddingMode.PKCS7。Java端虽然你写的是PKCS5Padding但心里要清楚这实际是PKCS7。在团队文档或接口定义中应该明确写明“双方均使用PKCS7填充模式”。如果使用的加密库如Bouncy Castle支持直接指定PKCS7Padding那就直接用这个更标准的名称。其他平台查阅文档寻找并设置PKCS7填充。2. 关键检查清单加解密参数四件套任何对称加解密确保以下四个核心参数在加密方和解密方完全一致算法与模式例如AES/CBC/PKCS7Padding。必须完整包含算法、分组模式、填充模式。密钥长度和字节内容必须一致。AES-128是16字节AES-256是32字节。初始化向量在CBC、CFB等模式下IV必须一致且通常需要随密文一起传输。IV不需要保密但必须是随机的。数据编码在将加密后的字节数组转换为字符串传输时如用Base64双方编解码方式要一致。3. 处理“恰好对齐”的数据如前所述PKCS7会在数据长度恰好为块大小整数倍时额外填充一个完整块。这意味着加密后的数据长度一定是块大小的整数倍。解密端必须能够正确处理这种情况。所有标准的、正确的PKCS7实现都会处理。自己手写填充/去除填充逻辑时极不推荐一定要考虑到这种边界情况否则解密最后一块完整数据时会失败。4. 一个具体的Java/C#互操作示例假设我们使用AES-128-CBC-PKCS7。Java端 (使用JCE):import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class JavaCrypto { public static String encrypt(String plainText, String keyBase64, String ivBase64) throws Exception { byte[] key Base64.getDecoder().decode(keyBase64); byte[] iv Base64.getDecoder().decode(ivBase64); byte[] input plainText.getBytes(StandardCharsets.UTF_8); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); // 注意这里写PKCS5Padding cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, AES), new IvParameterSpec(iv)); byte[] encrypted cipher.doFinal(input); return Base64.getEncoder().encodeToString(encrypted); } }实操心得在Java中虽然我们调用PKCS5Padding但团队内部沟通和接口文档上应称之为PKCS7模式并与合作方确认。C#端:using System; using System.Security.Cryptography; using System.Text; public class CSharpCrypto { public static string Encrypt(string plainText, string keyBase64, string ivBase64) { byte[] key Convert.FromBase64String(keyBase64); byte[] iv Convert.FromBase64String(ivBase64); byte[] input Encoding.UTF8.GetBytes(plainText); using (Aes aes Aes.Create()) { aes.Key key; aes.IV iv; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; // .NET中明确使用PKCS7 using (ICryptoTransform encryptor aes.CreateEncryptor()) { byte[] encrypted encryptor.TransformFinalBlock(input, 0, input.Length); return Convert.ToBase64String(encrypted); } } } }实操心得在C#中设置PaddingMode.PKCS7是标准做法。确保从Java端传来的IV也是Base64编码的并且解密时使用相同的Key、IV和PaddingMode。5. 调试与验证当出现解密失败时按以下步骤排查核对参数再次确认双方算法字符串、密钥长度/内容、IV内容、填充模式名称是否完全一致。检查数据长度获取加密后的密文Base64解码后检查其字节长度是否是块大小AES为16字节的整数倍。如果不是几乎可以肯定是填充或加密过程有问题。查看最后一个字节高级调试将密文Base64解码后查看最后一个字节的值。假设块大小16如果最后一个字节是0x01到0x10十进制1到16之间很可能是PKCS7填充。你可以手动验证填充是否正确。使用已知向量测试使用一个固定的、简单的明文如Hello、密钥和IV分别在两端进行加密比较输出的Base64密文是否完全一致。这是验证双方配置是否同步的最直接方法。7. 除了PKCS5/7其他填充模式简介PKCS7是最常用、最安全的填充模式之一但并非唯一。了解其他模式有助于在特定场景下做出选择。NoPadding规则不进行任何填充。要求明文长度必须是块大小的整数倍否则加密会出错。使用场景当你能够确保数据长度总是对齐时例如加密的是特定格式的、长度固定的协议数据包或者数据已经由上层协议处理好了填充。不推荐通用场景使用。ZerosPadding / ZeroBytePadding规则用字节0x00填充到块边界。问题如果原始数据末尾本身就有0x00解密时无法区分哪些是填充哪些是原始数据可能导致数据损坏。安全性较差不推荐使用。ISO10126Padding规则最后一个字节等于填充长度其余填充字节为随机数。优点由于填充字节是随机的能提供更好的安全性避免某些基于填充的预言攻击。注意并非所有平台都默认支持此填充。ANSIX923Padding规则最后一个字节等于填充长度其余填充字节为0x00。是ZerosPadding的一种改进至少能通过最后一个字节知道填充长度但仍有部分固定模式。选择建议对于绝大多数应用场景PKCS7Padding是默认的、推荐的选择。它在安全性、通用性和平台支持性上取得了最佳平衡。除非有非常明确的理由如兼容某个古老系统协议否则应坚持使用PKCS7。8. 从填充模式看加密生态的碎片化这次对PKCS5和PKCS7的深挖其实折射出密码学在工程实践中的一个典型问题标准、历史兼容性与API设计之间的摩擦。一个在标准定义上清晰明确的概念PKCS5用于8字节块由于历史原因和早期API的广泛采用在现实中变成了一个需要上下文理解的“方言”。Java的PKCS5Padding实际上在大多数情况下是PKCS7Padding这虽然方便了老代码的迁移却给学习者和新项目的跨平台集成带来了认知负担和潜在的陷阱。这提醒我们在从事加密相关开发时不要轻信示例代码很多博客里的示例代码只求“跑通”可能使用了不严谨的术语或存在隐藏的平台假设。深入理解标准文档对于核心概念最终极的权威是RFC或标准发布机构的文档。虽然枯燥但能避免误解。测试驱动兼容性在涉及多平台加解密的项目中建立完善的端到端测试用例覆盖各种边界情况空数据、对齐数据、长数据是保证稳定性的不二法门。回到我开头遇到的那个问题解决方案很简单将双方系统的填充模式明确约定为“PKCS7”并在Java端心里明确“我写的PKCS5Padding实际就是PKCS7”。同时在团队知识库中更新了加密组件规范明确要求所有新项目在接口文档中统一使用“PKCS7”这一术语避免了后续成员的困惑。一个小小的填充模式背后是密码学演进的历史和工程实践的细节搞清楚它通往稳定通信的道路就少了一块绊脚石。

相关新闻

MacOS下Docker部署Dify完整指南与优化实践

MacOS下Docker部署Dify完整指南与优化实践

1. MacOS环境准备与Docker安装在MacOS上部署Dify之前,需要先确保基础环境配置正确。我推荐使用Docker作为容器化解决方案,这能有效避免环境依赖冲突问题。以下是经过实测的完整配置流程:1.1 系统版本兼容性检查首先确认你的MacOS版本符合Dock…

2026/10/21 5:29:44 阅读更多 →
GPT-5.5与DeepSeek V4技术对比:从架构原理到开发实战应用

GPT-5.5与DeepSeek V4技术对比:从架构原理到开发实战应用

1. 项目概述:一场沸点社区的AI顶流“吐槽大会”最近在技术社区里,一场关于AI大模型的讨论热度居高不下,核心议题就是“GPT-5.5 vs DeepSeek V4”。这听起来像是一场官方发布会,但实际上,它更像是一场由社区开发者、产品…

2026/10/18 21:30:36 阅读更多 →
非华为电脑实现华为一碰传:NFC标签与蓝牙配网实战指南

非华为电脑实现华为一碰传:NFC标签与蓝牙配网实战指南

1. 项目缘起:当华为生态遇上非华为电脑作为一名数码爱好者,我手头的主力机一直是华为Mate系列,对它的多屏协同、一碰传这些生态功能依赖很深。但我的工作电脑是一台ThinkPad,家里还有一台MacBook。每次在手机和电脑之间传文件&…

2026/10/20 3:37:13 阅读更多 →

最新新闻

pyautogui图像识别驱动的桌面GUI自动化测试实战

pyautogui图像识别驱动的桌面GUI自动化测试实战

做自动化测试这些年,我一直习惯拿到新工具先找个能“看得见摸得着”的场景去练手。上次把 Python 的 pyautogui 重新捡起来,顺手拿 4399 小游戏当靶场,写了一套完全靠图像识别驱动的自动化脚本。整个过程中不抓包、不改内存、不碰游戏协议&am…

2026/10/21 8:02:13 阅读更多 →
杰理方案I2S时钟配置详解:从频率换算到故障排查

杰理方案I2S时钟配置详解:从频率换算到故障排查

上个月帮一个朋友调杰理方案的蓝牙音箱,板子外挂了一颗I2S接口的DAC,音质指标看着很漂亮。结果上电放歌第一秒我就知道要熬夜了——声音忽快忽慢,像磁带被手指捏着卷,里面还垫着一层细细的“滋滋”声。我没有急着翻代码&#xff0…

2026/10/21 8:02:13 阅读更多 →
基于Spring AI和RAG构建带引用溯源的知识库问答助手

基于Spring AI和RAG构建带引用溯源的知识库问答助手

1. AI 总是自己编答案?先把幻觉的病根说清楚做知识库问答助手的时候,我最大的痛点是 AI 一本正经地胡说八道。明明知识库里没有这个数据,它也能给你编出个一二三四来,语气还特别笃定。为了解决这个毛病,我把 Spring AI…

2026/10/21 8:02:13 阅读更多 →
SpringBoot+Vue构建旅游管理系统:架构设计、关键代码与答辩指南

SpringBoot+Vue构建旅游管理系统:架构设计、关键代码与答辩指南

先说结论:这是一个非常适合作为毕业设计的选题,技术栈主流、业务场景完整、工作量适中,而且旅游管理系统天然自带"前后端分离 数据可视化 业务状态流转"这些评审老师爱看的东西。我帮几个学弟学妹审过类似项目的代码,…

2026/10/21 8:02:13 阅读更多 →
Agent工程最小可行切片:从九天猜猜看状态管理与确定性设计

Agent工程最小可行切片:从九天猜猜看状态管理与确定性设计

1. “赋饭-九天猜猜”不是玩具,而是一次面向真实交互场景的Agent工程切片“赋饭-九天猜猜”这个标题乍看像某个校园社团的趣味活动代号,或是某款轻量级小程序的昵称——但如果你把它当成一个可有可无的Demo,那从第一行代码开始就走偏了。我带…

2026/10/21 8:02:13 阅读更多 →
构建跨会话跨Agent的语义上下文底座

构建跨会话跨Agent的语义上下文底座

1. 项目概述:为什么“跨会话上下文”成了当前智能体开发的硬伤最近在几个实际交付的智能体项目里,我反复被同一个问题卡住:用户上午问“帮我分析下这份销售报表”,下午接着问“上份报表里华东区的数据怎么变化的”,系统…

2026/10/21 8:01:13 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →