Java中正确使用Apache Commons Codec的DigestUtils进行MD5哈希
1. 从一次“撞库”风险说起为什么我们还在用MD5前阵子我负责的一个老系统进行安全审计发现用户密码模块还在用最基础的MD5直接加密存储。安全团队的报告毫不客气地标了“高危”——这几乎等于在数据库里明文存放密码。这让我重新审视了MD5这个“熟悉的陌生人”。在Java生态里提到MD5加密十有八九会想到Apache Commons Codec库里的DigestUtils工具类。它用起来太方便了一两行代码就能搞定以至于很多开发者包括曾经的我只记住了DigestUtils.md5Hex这个API却忽略了背后的一系列安全陷阱和适用场景。MD5Message-Digest Algorithm 5本质上是一种哈希函数或者说消息摘要算法。它的核心工作不是“加密”Encryption因为加密意味着可逆有密钥才能解密。而MD5是“哈希”Hashing是单向的理论上无法从生成的摘要那个32位的十六进制字符串反推出原始数据。它的设计初衷是确保数据的完整性比如你下载一个文件对比一下MD5值就能知道文件在传输过程中有没有被篡改。在Java中java.security.MessageDigest是进行消息摘要的官方入口。但直接使用它代码会显得有点啰嗦需要获取实例、更新数据、完成计算最后处理字节数组到十六进制字符串的转换。Apache Commons Codec的DigestUtils就是对这个过程极致的封装和简化让开发者能专注于业务逻辑。然而便利性也带来了认知上的懒惰。很多人误以为用了DigestUtils做MD5密码就安全了这恰恰是最大的误区。今天我就结合自己踩过的坑和后续的优化来彻底拆解一下在Java中如何正确、安全、有分寸地使用DigestUtils进行MD5操作。2. 深入DigestUtils不只是md5Hex那么简单DigestUtils类位于org.apache.commons.codec.digest包下。很多人对它的认知停留在两个静态方法md5Hex(String data)和md5Hex(byte[] data)。这确实是最常用的入口但理解它的完整能力才能应对更复杂的场景。2.1 核心方法解析与源码窥探我们直接看最常用的md5Hex(String data)内部做了什么。虽然不能直接贴全部源码但其核心逻辑链非常清晰字符串编码首先它将输入的字符串通过String.getBytes(StandardCharsets.UTF_8)转换为字节数组。这里有一个关键点它默认使用UTF-8编码。如果你的字符串包含非ASCII字符比如中文而上下游系统使用了不同的编码如GBK那么即使字符串看起来一样计算出的MD5值也会天差地别。这是第一个容易踩的坑。调用MessageDigest内部通过MessageDigest.getInstance(MD5)获取MD5算法实例。计算摘要调用digest方法得到16字节128位的摘要字节数组。十六进制转换最后使用Hex类同样是Commons Codec提供的工具将这个字节数组转换为长度为32的小写十六进制字符串。除了md5Hex还有一组对应的方法md5它返回的是原始的16字节byte[]。这在某些需要进一步处理比如作为其他加密算法的输入或者进行二进制比较的场景下更有用。import org.apache.commons.codec.digest.DigestUtils; public class DigestUtilsDemo { public static void main(String[] args) { String input Hello, MD5!; // 最常用的方式得到32位小写十六进制字符串 String md5HexString DigestUtils.md5Hex(input); System.out.println(md5Hex: md5HexString); // 输出类似e4d7f1b4ed2e42d15898f4b27b019da4 // 得到原始的16字节摘要数组 byte[] md5Bytes DigestUtils.md5(input); System.out.println(md5 bytes length: md5Bytes.length); // 输出16 // 处理字节数组输入如图片、文件流 byte[] fileData 模拟的文件字节数据.getBytes(); String fileMd5 DigestUtils.md5Hex(fileData); } }2.2 为什么选择DigestUtils而非原生MessageDigest我们对比一下原生写法和工具类写法原生MessageDigest写法import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class NativeMD5 { public static String toMd5Hex(String input) throws NoSuchAlgorithmException { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes()); // 需要手动转换字节数组到十六进制字符串 StringBuilder hexString new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(0xff b); if (hex.length() 1) { hexString.append(0); } hexString.append(hex); } return hexString.toString(); } }DigestUtils写法import org.apache.commons.codec.digest.DigestUtils; public class UtilsMD5 { public static String toMd5Hex(String input) { return DigestUtils.md5Hex(input); // 一行搞定 } }选择DigestUtils的理由显而易见代码简洁消除样板代码避免了繁琐的MessageDigest实例获取、异常处理NoSuchAlgorithmException在MD5场景下几乎不会抛出但必须捕获以及手动的十六进制转换。代码更专注于业务语义。性能与线程安全DigestUtils的静态方法内部每次调用都会创建新的MessageDigest实例。虽然有一些微小的对象创建开销但这保证了线程安全。对于绝大多数应用这点开销可忽略不计。如果你在极端高性能场景下需要对同一个MessageDigest实例进行复用才需要考虑自己维护实例。功能增强除了MD5DigestUtils还封装了SHA-1、SHA-256、SHA-512等常用哈希算法方法命名一致sha256Hex等大大降低了学习成本和使用门槛。编码一致性它固定使用UTF-8编码处理字符串避免了因平台默认编码不同导致的问题只要上下游都约定使用UTF-8就能保证结果一致。注意DigestUtils的便捷性也意味着控制权的让渡。例如你无法指定字符串的编码它强制UTF-8也无法在计算大文件摘要时使用update方法进行流式处理对于大文件应使用DigestUtils.md5Hex(InputStream)重载方法。这些限制在99%的场景下不是问题但需要心中有数。3. MD5的典型应用场景与安全边界理解了工具怎么用更要明白该在什么场合用。MD5的应用主要围绕“数据完整性校验”和“不可逆标识”展开但必须严格划定安全边界。3.1 安全场景文件完整性校验与数据标识这是MD5至今仍被广泛使用且安全的领域。软件包/文件分发验证这是MD5的“老本行”。官网在提供软件下载时通常会附带一个MD5或SHA-256校验和。用户下载后计算本地文件的MD5值与官网提供的对比一致则说明文件在传输过程中未被篡改。这里对抗的是传输错误或中间人攻击的篡改。数据库缓存键生成一个复杂的查询条件多个过滤条件、排序字段可以作为参数对象将其序列化为JSON字符串后计算MD5得到的哈希值作为Redis等缓存系统的Key。只要查询条件不变Key就不变完美命中缓存。MD5的碰撞风险在这里几乎可以忽略因为Key冲突的后果通常是缓存误命中可以通过设置较短的TTL或使用更安全的SHA-256来进一步降低风险。内容去重在网盘或内容管理系统中上传文件前先计算MD5。如果系统中已存在相同MD5的文件则无需重复存储物理文件只需建立一个新的引用指针即可节省大量存储空间。这依赖于MD5的“指纹”特性。// 场景示例生成缓存Key public String generateCacheKey(UserQuery query) { // 将查询对象转换为稳定的字符串表示例如排序后的JSON String queryString objectMapper.writeValueAsString(query); // 使用MD5生成固定长度的Key return user_query: DigestUtils.md5Hex(queryString); }3.2 危险禁区密码存储与弱数据签名绝对不要单独使用MD5存储密码这是我用血泪教训换来的经验。彩虹表攻击MD5计算速度极快这反而成了它的弱点。攻击者可以预先计算海量常用密码及其对应MD5值做成“彩虹表”。拿到你的密码MD5后直接查表就能反推出原始密码。password123这样的密码其MD5值在彩虹表中是秒破的。碰撞攻击从密码学上MD5已被证实存在碰撞漏洞即可以人为制造出两个不同的数据拥有相同的MD5值。虽然针对任意给定摘要构造碰撞仍有难度但这已彻底摧毁了MD5在需要抗碰撞的安全协议如数字证书中的可信度。那么密码应该怎么存答案是加盐哈希。盐Salt是一个随机生成的长字符串每个用户都不一样。存储时不是存MD5(密码)而是存盐值和哈希算法(盐值 密码)。验证时用同样的盐值和用户输入的密码重新计算哈希与存储的比对。// 伪代码正确的密码存储姿势使用BCrypt或PBKDF2等慢哈希函数 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String rawPassword userPassword123; // 加密每次结果都不同因为内置了随机盐 String encodedPassword encoder.encode(rawPassword); // 存储 encodedPassword 到数据库 // 验证 boolean matches encoder.matches(rawPassword, storedEncodedPassword);现代的密码哈希算法如BCrypt、Scrypt、Argon2被设计为“慢”且可调节成本专门对抗彩虹表和暴力破解。DigestUtils.md5Hex在这种场景下是完全错误的工具。3.3 性能考量MD5真的快吗在需要快速计算大量数据摘要的场景比如实时日志去重、流数据分片MD5的速度是一个优势。但“快”是相对的也需要量化。我做了一个简单的基准测试使用JMH这里简化为逻辑描述对比SHA-256对同一段1MB的数据MD5的计算速度大约是SHA-256的2-3倍。在每秒需要处理成千上万次哈希操作的场景这个差异会累积成显著的性能区别。瓶颈往往不在哈希计算在大多数Web应用中一次MD5计算的时间微秒级与数据库查询、网络I/O毫秒级相比几乎可以忽略不计。因此除非是极端性能敏感的系统否则不必纠结于MD5和SHA-256之间的性能差安全性应优先考虑。4. 实战DigestUtils的进阶用法与避坑指南掌握了基础我们来看看在实际项目中如何更高效、更稳健地使用DigestUtils。4.1 处理大文件与输入流直接读取大文件到内存再计算MD5是灾难性的。DigestUtils提供了对InputStream的支持可以流式处理内存友好。import org.apache.commons.codec.digest.DigestUtils; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class FileMd5Calculator { public static String calculateFileMd5(String filePath) throws IOException { // 使用try-with-resources确保流关闭 try (InputStream is new FileInputStream(filePath)) { // 这个方法内部会分块读取流并更新摘要直到流结束 return DigestUtils.md5Hex(is); } // 注意这个方法会消费掉输入流如果流还需要用于其他用途需要额外处理。 } }避坑提示DigestUtils.md5Hex(InputStream)方法在读取流后不会重置流。如果你的程序需要先计算MD5再用原始流做其他事情比如上传到云存储你需要要么使用BufferedInputStream并配合mark()和reset()方法如果流支持要么将流读入字节数组如果文件不大或者更常见的做法——先保存到临时文件然后基于文件流进行多次读取。4.2 确保跨系统一致性编码与格式如前所述字符串的编码是MD5结果一致性的生命线。场景你的Java服务用DigestUtils.md5Hex计算了一个字符串的MD5发送给一个Python或PHP服务进行校验对方校验失败。排查99%的问题出在编码上。DigestUtils.md5Hex(String)使用UTF-8。请确认对方在计算MD5前也将字符串明确编码为UTF-8字节。在Python中应使用string.encode(utf-8)在PHP中确保内部编码是UTF-8或使用mb_convert_encoding。解决方案在系统间传递需要计算哈希的字符串时最好附带明确指定编码如UTF-8的说明文档。对于关键接口可以约定双方都先对字符串进行Base64编码同样是UTF-8字节的Base64再计算MD5多一层保障。// 强制指定编码的示例虽然DigestUtils内部是UTF-8但显式化更清晰 public static String md5HexWithEncoding(String data, String charsetName) throws UnsupportedEncodingException { // 实际上DigestUtils没有提供指定编码的方法。 // 如果必须使用非UTF-8编码需要回到原生MessageDigest MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(data.getBytes(charsetName)); // 例如 GBK return Hex.encodeHexString(digest); // 使用Commons Codec的Hex类 }4.3 从MD5平滑升级到更安全的哈希算法如果你的老系统里遍布着MD5而安全要求升级了怎么办全部重写成本太高。一个可行的策略是双轨制与渐进迁移。识别场景首先区分哪些场景是必须升级的如新的密码存储、安全令牌哪些是可以暂时保留的如内部非敏感的文件校验。设计新接口创建一个新的工具类或方法默认使用SHA-256。例如SecurityHashUtil.sha256Hex(String data)。数据库字段扩展对于用户密码这类数据在数据库表中增加一个新字段如password_hash_v2用于存储新算法如BCrypt生成的哈希。同时保留旧字段。迁移逻辑在用户下次登录时用旧MD5验证成功后立即用新算法计算哈希并存入新字段然后将旧字段置空或标记为已迁移。这样随着时间推移所有活跃用户的密码都自动迁移到了新算法。更新校验逻辑对于文件校验等场景可以在接口中同时支持MD5和SHA-256两个校验和新上传的文件要求提供SHA-256旧文件暂时仍用MD5验证逐步过渡。// 示例支持多种算法的工具类 public class HashUtil { public enum Algorithm { MD5, SHA256, SHA512 } public static String hash(String data, Algorithm algorithm) { switch (algorithm) { case SHA256: return DigestUtils.sha256Hex(data); case SHA512: return DigestUtils.sha512Hex(data); case MD5: default: return DigestUtils.md5Hex(data); // 默认或兼容旧逻辑 } } }4.4 常见问题排查清单在实际开发中遇到MD5相关的问题可以按以下清单排查问题现象可能原因解决方案与第三方/其他语言计算结果不一致1. 字符串编码不同如UTF-8 vs GBK。2. 字符串首尾存在不可见空白字符空格、换行。3. 对方或己方计算的是包含BOM字节顺序标记的UTF-8。1. 统一使用UTF-8编码并显式指定。2. 在计算前对字符串进行trim()但需确认业务逻辑是否允许。3. 检查并去除BOM。计算大文件时内存溢出OOM使用了DigestUtils.md5Hex(byte[])试图将整个文件读入内存。改用DigestUtils.md5Hex(InputStream)进行流式处理。相同的输入每次运行结果都不同输入中包含了随机或可变因素例如时间戳、UUID、未初始化的变量。检查用于计算MD5的源数据是否真正“恒定”。使用调试工具打印出计算前的字符串。性能瓶颈CPU占用高在循环或高频接口中对大量或超长字符串进行MD5计算。1. 评估是否真的需要实时计算能否缓存结果。2. 对于超长文本考虑先进行摘要或截断需评估业务影响。3. 使用性能分析工具定位热点。最后关于lombok报错“you aren‘t using a compiler supported by lombok”以及“OutOfMemoryError”这些虽然出现在你的热词列表里但与DigestUtils和MD5加密本身无直接关联。前者是Lombok注解处理器与IDE或构建工具Maven/Gradle的编译器版本不匹配问题通常升级Lombok版本或配置构建工具即可解决。后者是JVM堆内存不足需要调整-Xmx参数或排查内存泄漏。在计算极大文件的MD5时如果错误地采用一次性加载到内存的方式确实可能引发OOM这再次强调了使用InputStream版本的重要性。DigestUtils是一个优秀的工具它把复杂的MessageDigestAPI简化成了一行优雅的代码。但作为开发者我们必须清醒地认识到工具本身不负责安全安全来自于使用工具的人对场景的深刻理解。MD5在数据完整性校验和非敏感标识领域依然可靠高效但在密码学安全领域它已经光荣退役。下次当你顺手写下DigestUtils.md5Hex时不妨花一秒钟想想这个场景它真的还合适吗

相关新闻

Unity AR圆柱环游交互开发:从空间计算到跨平台实现

Unity AR圆柱环游交互开发:从空间计算到跨平台实现

1. 项目概述:当AR遇上圆柱环游最近在做一个挺有意思的AR项目,核心需求是让用户通过手机或AR眼镜,围绕一个虚拟的圆柱体进行“环游”式交互。听起来有点抽象?简单说,就是用户拿着设备,绕着这个虚拟的圆柱走或…

2026/10/16 3:28:08 阅读更多 →
Unity 2022.3.34元数据兼容性解析:Cpp2IL逆向工具适配指南

Unity 2022.3.34元数据兼容性解析:Cpp2IL逆向工具适配指南

1. 项目概述:当Cpp2IL遇上Unity 2022.3.34如果你是一名从事Unity游戏逆向分析、安全审计或者想研究某些商业游戏内部逻辑的开发者,那么“Cpp2IL”这个名字对你来说一定不陌生。它是一个强大的工具,能够将Unity游戏编译后的C代码(更…

2026/10/17 16:22:45 阅读更多 →
终极MOD管理方案:三步掌握虎符台,轻松管理全面战争游戏MOD

终极MOD管理方案:三步掌握虎符台,轻松管理全面战争游戏MOD

终极MOD管理方案:三步掌握虎符台,轻松管理全面战争游戏MOD 【免费下载链接】legion-seal 虎符台/Legion Seal,全面战争游戏MOD管理器,技术栈:Tauri 2 Vue TailwindCSS 项目地址: https://gitcode.com/zeyl/legion-…

2026/10/14 14:50:03 阅读更多 →

最新新闻

韩语词汇表构建与评测:从词表设计到分词、拼写检查与检索召回

韩语词汇表构建与评测:从词表设计到分词、拼写检查与检索召回

简介:这份韩语词汇表资源面向韩语学习者、语言研究者及需要构建韩语词典数据的开发者,提供从原始词条到结构化词典的完整数据集合。资源包共5个文件,以json词典数据、txt单词列表和md说明文档为主,压缩包约28.37MB,jso…

2026/10/17 21:22:31 阅读更多 →
深度实战 Codex Potter:用 AGENTS.md 与 Ralph Loop 构建自主对齐的 AI 编程工作流,让 Codex 持续工作一整天

深度实战 Codex Potter:用 AGENTS.md 与 Ralph Loop 构建自主对齐的 AI 编程工作流,让 Codex 持续工作一整天

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

2026/10/17 21:22:31 阅读更多 →
在openclaw(原Clawdbot)中配置 TaoToken API 的完整实践

在openclaw(原Clawdbot)中配置 TaoToken API 的完整实践

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

2026/10/17 21:22:31 阅读更多 →
PostgreSQL JSONB与MyBatis-Plus自动注入方案详解

PostgreSQL JSONB与MyBatis-Plus自动注入方案详解

先亮个结论:PostgreSQL 里的 JSONB 是真好用,但放到 Java 生态,尤其是我平时用得最多的 MyBatis-Plus 里,你会发现默认行为经常“对不上号”。实体里一个 Map 或自定义对象,数据库里一个 jsonb 列,插件默认…

2026/10/17 21:22:31 阅读更多 →
算法竞赛入门经典例题答案:从对答案到独立解题的完整指南

算法竞赛入门经典例题答案:从对答案到独立解题的完整指南

简介:这份资源是《算法竞赛入门经典(第二版)》例题答案与配套代码合集,面向准备ACM/ICPC等算法竞赛的初学者与进阶选手,帮助读者对照书中例题检验思路、补齐实现细节。压缩包共224个文件,以191个cpp源码为主…

2026/10/17 21:22:31 阅读更多 →
谷歌云2025 AI商业趋势白皮书:从趋势信号到商业决策

谷歌云2025 AI商业趋势白皮书:从趋势信号到商业决策

简介:《人工智能行业:2025年AI商业趋势白皮书》是谷歌云面向企业高管、战略规划者与AI从业者发布的战略研究报告,聚焦2025年重塑商业世界的五大AI趋势,为制定AI路线图提供数据洞察与行动建议。资源共1个PDF文件,压缩包…

2026/10/17 21:21:30 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →