Java实现HMAC-SHA256签名:从PHP hash_hmac迁移的完整指南
1. 项目概述从PHP的便捷到Java的严谨在Web开发、API接口调用和系统间安全通信的场景里数据签名是确保信息完整性和来源真实性的基石。很多从PHP转向Java的开发者初期都会怀念PHP里那个hash_hmac函数——一行代码指定算法和密钥签名瞬间生成简单得让人感动。然而当他们在Java项目中需要实现同样的HMAC-SHA256签名时却常常陷入NoSuchAlgorithmException、编码混乱、结果不一致等“坑”里。这并非Java能力不足而是其强类型、显式管理的特性要求开发者必须更清晰地理解每一步在做什么。本文将从实际需求出发手把手带你用Java实现与PHPhash_hmac(‘sha256’, $data, $key)完全等效的签名逻辑并深入剖析那些容易踩坑的细节让你不仅写出能跑的代码更能写出健壮、可靠的代码。2. 核心原理与方案选型为什么是HMAC-SHA256在开始写代码之前我们必须搞清楚我们到底在做什么。签名不是加密它的目的不是隐藏数据而是为数据生成一个唯一的、不可伪造的“指纹”。2.1 HMAC与SHA-256的角色解析HMAC即基于哈希的消息认证码。你可以把它理解为一个更安全的“盖章”流程。单纯的SHA-256哈希就像用一个公开的模具算法对数据蜡压出一个印迹。如果别人知道了模具他可以用你的数据甚至伪造的数据压出同样的印迹无法证明来源。而HMAC引入了一个密钥$key这个密钥就像盖章人的私章被混入到“压模”的过程中。最终生成的签名是数据和密钥共同作用的结果。不知道密钥攻击者就无法伪造出有效的签名。SHA-256是哈希算法的一种它接收任意长度的输入生成一个固定长度256位即32字节的、看似随机的字符串哈希值。它具有“雪崩效应”输入哪怕只改变一个比特输出也会截然不同且理论上不可逆。HMAC-SHA256就是用SHA-256作为这个哈希算法的HMAC实现。选择HMAC-SHA256是因为它在安全性和性能上取得了很好的平衡被广泛应用于JWT、支付接口、OAuth 2.0等众多协议和场景中是当前事实上的标准选择之一。2.2 Java实现方案对比Mac类 vs 手动实现Java标准库javax.crypto已经为我们提供了完善的HMAC支持核心类是javax.crypto.Mac。这是官方推荐且最安全、最便捷的实现方式。为什么不手动拼接密钥和数据进行哈希HMAC的标准定义RFC 2104包含对密钥的预处理如果密钥过长则先哈希过短则补零以及内外两层哈希的结构。手动实现极易在此处出错导致与标准库或其他语言如PHP的结果不一致。使用Mac类这些复杂的步骤都由经过严格验证的底层库完成我们只需关注业务逻辑。因此我们的方案非常明确使用javax.crypto.Mac类指定算法为HmacSHA256。3. 保姆级代码实现与逐行解读理论清晰后我们进入实战环节。下面我将提供一个工具类并逐行解释其关键点。import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; /** * HMAC-SHA256 签名工具类 * 提供与PHP hash_hmac(sha256, data, key) 等效的功能 */ public class HmacSha256Util { /** * 生成HMAC-SHA256签名返回十六进制小写字符串 * * param data 待签名的原始数据字符串 * param key 用于签名的密钥字符串 * return 十六进制格式的签名 * throws RuntimeException 当算法不支持或密钥无效时抛出 */ public static String sign(String data, String key) { try { // 1. 获取Mac实例并指定算法 Mac mac Mac.getInstance(HmacSHA256); // 2. 将字符串密钥转换为字节数组并创建密钥规范 // 注意这里直接使用原始字符串的UTF-8字节与PHP的hash_hmac行为一致 SecretKeySpec secretKeySpec new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HmacSHA256); // 3. 用密钥初始化Mac实例 mac.init(secretKeySpec); // 4. 计算签名传入数据的UTF-8字节 byte[] rawHmac mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); // 5. 将字节数组转换为十六进制字符串小写 return HexFormat.of().formatHex(rawHmac); // 对于Java 8及以下版本可以使用以下替代方案 // StringBuilder hexString new StringBuilder(); // for (byte b : rawHmac) { // String hex Integer.toHexString(0xff b); // if (hex.length() 1) { // hexString.append(0); // } // hexString.append(hex); // } // return hexString.toString(); } catch (NoSuchAlgorithmException e) { // “HmacSHA256”是JRE标准算法通常不会抛出此异常。 // 如果抛出说明运行环境异常。 throw new RuntimeException(当前Java环境不支持HmacSHA256算法, e); } catch (InvalidKeyException e) { // 密钥不符合规范时抛出例如为null throw new RuntimeException(无效的签名密钥, e); } } /** * 验证签名是否匹配 * * param data 原始数据 * param key 密钥 * param signature 待验证的签名十六进制字符串 * return 签名是否有效 */ public static boolean verify(String data, String key, String signature) { // 关键采用恒定时间比较避免时序攻击 String calculatedSign sign(data, key); return MessageDigest.isEqual( calculatedSign.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8) ); // 注意Java 17 的 HexFormat.of().formatHex 返回小写十六进制。 // 如果待验证的签名可能是大写需要先统一转为小写再比较signature.toLowerCase(Locale.ROOT) } }逐行解读与避坑点Mac.getInstance(“HmacSHA256”)这是获取算法实例的标准方式。确保字符串拼写完全正确。在Android或某些定制化JRE中如果缺少相应的Provider可能会抛出NoSuchAlgorithmException但标准Oracle/OpenJDK JRE都包含它。SecretKeySpec的创建这是第一个大坑。SecretKeySpec的第二个参数是算法名这里传入”HmacSHA256”它告诉密钥规范这个密钥是用于哪种MAC算法的。虽然有些代码这里写”AES”也能跑因为SecretKeySpec不强制校验但为了语义清晰和未来兼容性务必与Mac.getInstance的算法名保持一致。key.getBytes(StandardCharsets.UTF_8)这是第二个也是最容易导致与PHP结果不一致的天坑。PHP的hash_hmac函数其$data和$key参数都是字符串。在PHP内部字符串是带有编码的字节序列。当你不特意指定时PHP会使用其内部的字符编码如ISO-8859-1或UTF-8取决于脚本文件和配置将字符串转换为字节。为了确保跨语言一致性我们必须明确指定编码。StandardCharsets.UTF_8确保了无论JVM默认编码是什么我们都使用UTF-8进行转换这与现代Web开发中PHP默认使用UTF-8编码的趋势是一致的。如果你的PHP项目明确使用了其他编码如GBK那么Java端也需要使用对应的Charset.forName(“GBK”)。HexFormat.of().formatHex(rawHmac)这是Java 17引入的官方十六进制转换工具简洁高效。它生成的是小写十六进制字符串。PHP的hash_hmac函数默认返回的也是小写十六进制。如果你需要大写可以使用.toUpperCase()。对于Java 8用户需要用注释中的循环手动转换注意0xff b的操作是为了将byte的负值转换为正确的无符号整数。验证方法中的MessageDigest.isEqual这是至关重要的安全实践。比较两个签名是否相等时不能使用普通的字符串equals()方法。因为equals()方法在发现第一个字符不同时会立即返回false攻击者可以通过测量比较所花费的时间来逐步猜测出正确的签名这种攻击称为“时序攻击”。MessageDigest.isEqual方法采用了恒定时间比较算法无论是否匹配其执行时间都是基本相同的从而封堵了这种旁路攻击渠道。4. 高级场景与配置详解在实际项目中我们的数据和密钥可能不是简单的字符串或者我们需要更精细的控制。4.1 处理非字符串数据和密钥有时待签名的数据可能是JSON对象、Map或者密钥是从文件、环境变量中读取的字节数组。// 场景1签名一个复杂对象如转换为JSON字符串 public static String signObject(Object obj, String key) { ObjectMapper mapper new ObjectMapper(); // 使用Jackson库 try { String jsonData mapper.writeValueAsString(obj); return sign(jsonData, key); } catch (JsonProcessingException e) { throw new RuntimeException(对象序列化失败, e); } } // 关键必须确保对象序列化为字符串的规则空格、键序等与对接方完全一致。 // 例如JSON中字段的排序、是否格式化缩进都会影响最终的签名。 // 场景2密钥是字节数组例如从Base64解码或随机生成 public static String signWithBytes(String data, byte[] keyBytes) { try { Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec secretKeySpec new SecretKeySpec(keyBytes, HmacSHA256); mac.init(secretKeySpec); byte[] rawHmac mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(rawHmac); } catch (Exception e) { throw new RuntimeException(签名失败, e); } } // 注意如果密钥是Base64编码的字符串需要先解码byte[] keyBytes Base64.getDecoder().decode(base64Key);4.2 算法提供者与性能考量默认情况下Mac会使用JRE中优先级最高的安全提供者通常是SunJCE。在极端注重性能或需要特定硬件加速的场景下你可以指定提供者。public static String signWithProvider(String data, String key) { try { // 获取名为“SunJCE”的提供者 Provider provider Security.getProvider(SunJCE); Mac mac Mac.getInstance(HmacSHA256, provider); // ... 后续初始化与计算同上 } catch (Exception e) { // 回退到默认提供者 return sign(data, key); } }对于绝大多数应用默认提供者已完全足够。只有在明确知道目标运行环境如某款硬件安全模块HSM提供了优化实现时才需要考虑指定提供者。4.3 线程安全与实例复用Mac实例在调用init()方法初始化后是线程安全的。这意味着对于同一个密钥你可以创建一个Mac实例并在多线程环境中重复使用它来调用doFinal()方法这能避免反复初始化的开销在高并发场景下提升性能。public class HmacSha256Signer { private final Mac mac; public HmacSha256Signer(String key) throws InvalidKeyException, NoSuchAlgorithmException { this.mac Mac.getInstance(HmacSHA256); SecretKeySpec spec new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HmacSHA256); this.mac.init(spec); } public String sign(String data) { byte[] result this.mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(result); } } // 使用方式在服务启动时初始化一个Signer实例然后注入到各个需要用的组件中。注意Mac实例在调用doFinal()后其状态仍然有效可以继续用于下一次计算。这与MessageDigest不同MessageDigest在digest()后需要调用reset()。但如果你需要切换密钥则必须重新调用init()方法。5. 跨语言联调与问题排查实战这是问题的高发区。经常出现“我自己测没问题和对端一联调就失败”的情况。请按照以下清单系统性排查。5.1 签名不一致排查清单当你发现Java生成的签名与PHP或其他语言生成的签名不同时请按顺序检查以下每一项数据源是否100%相同肉眼欺骗字符串末尾是否有不可见的空格、换行符\n,\r\n在IDE里打开显示空白字符的功能检查。编码陷阱数据是否包含中文等非ASCII字符双方是否明确统一使用了UTF-8编码用以下代码在双方打印字节数组进行比对是最可靠的方式。System.out.println(Arrays.toString(data.getBytes(StandardCharsets.UTF_8)));在PHP中使用bin2hex($data)或unpack(‘H*’, $data)输出十六进制字节进行比较。密钥处理是否一致密钥本身是否包含特殊字符或空格密钥是否被Base64编码或URL编码过对方提供的是编码后的字符串还是原始字符串核心双方将密钥字符串转换为字节数组时使用的字符编码是否相同强制约定并使用UTF-8是避免绝大多数问题的银弹。算法和输出格式是否匹配算法确认是HMAC-SHA256不是SHA256或HMAC-SHA1。输出是十六进制hex还是Base64PHP的hash_hmac第三个参数设为true会输出原始二进制设为false默认输出十六进制小写。我们的Java工具类输出的是十六进制小写。如果需要Base64使用Base64.getEncoder().encodeToString(rawHmac)。是否有多余的步骤对方或你自己是否在签名前对数据进行了URL编码、排序如按字典序排序参数等预处理这些步骤必须完全一致。签名后是否对签名结果又进行了一次编码如再次Base645.2 实战调试示例假设我们与一个PHP服务联调对方给出的示例是$key ‘secret’; $data ‘message’; $sign hash_hmac(‘sha256’, $data, $key); echo $sign; // 输出8b5f48702995c1598c573db1e21866a9abb4e8d11e0839d4be94a48e1ffbb5f1我们的Java代码sign(“message”, “secret”)却得到了不同的结果。调试步骤在Java端打印密钥和数据的字节System.out.println(“Key bytes: ” Arrays.toString(“secret”.getBytes(StandardCharsets.UTF_8))); System.out.println(“Data bytes: ” Arrays.toString(“message”.getBytes(StandardCharsets.UTF_8)));输出应为Key bytes: [115, 101, 99, 114, 101, 116]和Data bytes: [109, 101, 115, 115, 97, 103, 101]。在PHP端确保脚本文件保存为UTF-8无BOM格式然后添加调试代码$key ‘secret’; $data ‘message’; echo ‘Key hex: ‘ . bin2hex($key) . “\n”; echo ‘Data hex: ‘ . bin2hex($data) . “\n”;运行PHP脚本观察输出。如果输出也是Key hex: 736563726574和Data hex: 6d657373616765这是上面Java字节数组的十六进制表示那么说明数据源一致。如果PHP输出不同比如因为文件是GBK编码导致中文密钥的字节表示不同那么就需要统一编码。确保PHP脚本顶部有header(‘Content-Type: text/html; charsetutf-8’);并且文件本身是UTF-8编码。如果数据源一致那么问题可能出在算法调用或输出上。检查PHP是否使用了hash_hmac(‘sha256’, $data, $key, false)默认以及Java是否使用了正确的HmacSHA256。通过这种逐字节比对的方法几乎可以定位所有跨语言签名不一致的问题。6. 生产环境最佳实践与安全加固将代码用于生产环境时不能只满足于功能正确还需考虑安全性和健壮性。6.1 密钥管理绝不能硬编码密钥是签名的灵魂必须妥善保管。错误示范String key “mySuperSecretKey123!”;直接写在代码里。正确做法从环境变量读取String key System.getenv(“API_HMAC_KEY”);从安全的配置中心如Spring Cloud Config, Apollo读取。从密钥管理服务如AWS KMS, Azure Key Vault, 阿里云KMS动态获取。在应用启动时注入而不是在每次签名时去读取。6.2 异常处理与日志记录工具类中我们抛出了RuntimeException在实际业务中最好定义业务异常并进行恰当的日志记录但要注意不要将密钥或完整的原始数据记录到日志中以防信息泄露。public class SignException extends Exception { public SignException(String message, Throwable cause) { super(message, cause); } } public String signSafely(String data, String key) throws SignException { try { return sign(data, key); } catch (RuntimeException e) { // 记录错误原因但不记录敏感数据 log.error(“HMAC签名失败原因{}”, e.getMessage()); // 可以在此处埋点监控 throw new SignException(“生成签名失败”, e); } }6.3 签名验证的强化前面提到的verify方法使用了恒定时间比较。此外还可以考虑签名有效期在待签名数据中融入时间戳如data originalData “timestamp” ts验证签名时同时检查时间戳是否在允许的窗口期内如5分钟防止重放攻击。随机数Nonce同样在数据中融入一个一次性随机字符串服务端缓存已使用过的Nonce防止同一签名被重复使用。6.4 性能监控与调优对于超高并发的API网关或认证中心签名验证可能是性能瓶颈之一。监控对sign和verify方法的耗时进行监控使用Micrometer, SLF4J定时器等。缓存对于频繁验证且短时间内内容不变的请求如携带JWT的请求可以考虑在验证签名后将(数据, 签名)的组合缓存一段时间几分钟直接返回缓存结果。异步/批量处理如果场景允许可以将一批数据的签名验证任务收集起来利用Mac的线程安全性在后台线程池中进行批量处理。从怀念PHP的hash_hmac到在Java中游刃有余地实现它关键在于理解其背后的原理和细节。编码一致性、密钥管理、安全比较这些点比调用一个函数本身更重要。希望这份指南不仅能让你写出正确的代码更能让你理解每一行代码背后的“为什么”从而构建出更安全、更稳定的系统。下次再遇到签名问题你大可以自信地说“来我们逐字节对一下。”

相关新闻

C++17 std::shared_ptr数组支持详解:原理、应用与性能优化

C++17 std::shared_ptr数组支持详解:原理、应用与性能优化

1. 项目概述:为什么C17的std::shared_ptr对数组如此重要?如果你和我一样,是从C98/03甚至更早的版本一路写过来的,那么对智能指针管理数组这件事,多半会有点“心理阴影”。在C11/14的时代,我们有了std::shar…

2026/10/17 9:41:28 阅读更多 →
C/C++项目结构规范:从混乱到优雅的模块化设计指南

C/C++项目结构规范:从混乱到优雅的模块化设计指南

1. 项目概述:为什么我们需要一个优雅的C/C项目结构规范?在C和C的世界里摸爬滚打十几年,我见过太多“一次性”项目。它们往往始于一个简单的main.c,随着功能堆叠,逐渐演变成一个包含数百个文件的、名为“src”的文件夹&…

2026/10/17 9:41:31 阅读更多 →
Gauss-Laplacian滤波:图像边缘检测的经典算法与OpenCV实现

Gauss-Laplacian滤波:图像边缘检测的经典算法与OpenCV实现

1. 项目概述:为什么需要Gauss-Laplacian滤波?在图像处理的实际项目中,我们常常会遇到一个看似矛盾的需求:既要平滑掉图像中的噪声,又要清晰地保留住物体的边缘。这就像给一张老照片做修复,你希望去除掉那些…

2026/10/17 9:42:03 阅读更多 →

最新新闻

基于Spark的电影推荐系统:混合推荐算法与工程实践

基于Spark的电影推荐系统:混合推荐算法与工程实践

简介:这份资源是面向大数据与推荐系统方向学习者、毕业设计选题学生的完整项目资料,围绕基于Spark的电影推荐系统展开,重点解决海量影片场景下个性化筛选与精准推荐的问题。内容涵盖绪论、开发技术介绍、系统分析与设计、系统实现及测试等章节…

2026/10/17 17:28:24 阅读更多 →
Vue动态组件多表单校验:注册表模式解决子父组件验证失灵

Vue动态组件多表单校验:注册表模式解决子父组件验证失灵

简介:这份资源面向使用 Vue 进行前端开发、需要处理选项卡或下拉框切换场景的开发者,提供动态绑定组件与子父组件多表单联合验证的实现代码。它针对的是用 if/else 或 switch 判断加载组件时产生大量冗余代码、变量定义分散且难以维护的问题,…

2026/10/17 17:28:24 阅读更多 →
Transformer模型详解:从注意力公式到能跑通的极简实现

Transformer模型详解:从注意力公式到能跑通的极简实现

简介:这份《Transformer 模型详解》PDF 面向自然语言处理初学者、深度学习入门者及需要系统梳理注意力机制的算法工程师,帮助读者从零理解 Transformer 的整体架构与核心原理。资源为单个 PDF 文件,压缩包约 2.01MB,内容以图文结合…

2026/10/17 17:27:23 阅读更多 →
Vue项目三步部署指南:Ubuntu + Nginx从构建到线上发布

Vue项目三步部署指南:Ubuntu + Nginx从构建到线上发布

看到标题里的“ubnutu”,我先说个小事:这应该是 ubuntu 的笔误。不过这并不影响思路,下面我统一用 ubuntu 22.04 来讲。很多前端同学开发完 vue 项目,npm run build 一执行,dist 目录出来了,然后就卡住了—…

2026/10/17 17:27:23 阅读更多 →
内置ALC的音频前置放大器D2538A和D3308

内置ALC的音频前置放大器D2538A和D3308

应用领域D2538A和D3308是两款内置ALC(音频限幅器)的前置音频放大器芯片,其中D2538A为单通道,D3308为双通道,它特别适用于胎心仪、个人医疗防护、立体声收录机、盒式录音机等涉及音频放大与限幅的产品方案。功能介绍音频…

2026/10/17 17:26:23 阅读更多 →
FAIR风险量化实战:从模糊判断到可校准的年化损失建模

FAIR风险量化实战:从模糊判断到可校准的年化损失建模

简介:本资源是一份面向信息安全从业者、风控工程师与技术决策者的FAIR风险量化分析实战指南,聚焦解决企业级信息风险难以精准建模与量化评估的痛点。文档系统拆解FAIR框架核心要素(威胁源、脆弱性、资产、影响)、风险计算模型、蒙…

2026/10/17 17:26:23 阅读更多 →

日新闻

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 阅读更多 →