<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; https</title>
	<atom:link href="http://tonybai.com/tag/https/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 12 Apr 2026 22:30:28 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>权威认证：Go核心密码学库通过独立安全审计</title>
		<link>https://tonybai.com/2025/05/21/go-crypto-audit/</link>
		<comments>https://tonybai.com/2025/05/21/go-crypto-audit/#comments</comments>
		<pubDate>Wed, 21 May 2025 13:20:13 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Argon2]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[ECC]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[ECDSA]]></category>
		<category><![CDATA[FIPS-140]]></category>
		<category><![CDATA[fips-203]]></category>
		<category><![CDATA[FIPS-204]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HMAC]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[KEM]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[PQC]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Shor]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[X25519MLKEM768]]></category>
		<category><![CDATA[后量子密码]]></category>
		<category><![CDATA[后量子密码学]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[量子计算]]></category>
		<category><![CDATA[随机数]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4737</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/21/go-crypto-audit 大家好，我是 Tony Bai。 信息安全是我们数字时代的基石。对于 Go 语言而言，其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日，Go 官方博客发布了一篇重要文章，详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的安全审计结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入，也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。 好消息是：审计结果非常积极！ 仅发现一个低风险问题（已在 Go 1.25 开发分支修复，且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成）和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。 在这篇文章中，我们就来介绍这一对Go密码学领域具有里程碑意义的事件。 安全审计的范围与 Go 的密码学设计原则 Trail of Bits 的审计范围广泛，涵盖了 Go 标准库中核心的加密组件，这些组件同时也是新的原生FIPS 140-3模块的验证部分。具体包括： 密钥交换： ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。 数字签名： ECDSA, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-crypto-audit-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/21/go-crypto-audit">本文永久链接</a> &#8211; https://tonybai.com/2025/05/21/go-crypto-audit</p>
<p>大家好，我是 Tony Bai。</p>
<p>信息安全是我们数字时代的基石。对于 Go 语言而言，其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日，<a href="https://go.dev/blog/tob-crypto-audit">Go 官方博客发布了一篇重要文章</a>，详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的<strong>安全审计</strong>结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入，也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。</p>
<p><strong>好消息是：审计结果非常积极！</strong> 仅发现一个低风险问题（已在 Go 1.25 开发分支修复，且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成）和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。</p>
<p>在这篇文章中，我们就来介绍这一对Go密码学领域具有里程碑意义的事件。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<h2>安全审计的范围与 Go 的密码学设计原则</h2>
<p>Trail of Bits 的审计范围广泛，涵盖了 Go 标准库中核心的加密组件，这些组件同时也是新的原生<a href="https://tonybai.com/2024/11/16/go-crypto-and-fips-140">FIPS 140-3</a>模块的验证部分。具体包括：</p>
<ul>
<li><strong>密钥交换：</strong> ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。</li>
<li><strong>数字签名：</strong> ECDSA, RSA, 和 Ed25519。</li>
<li><strong>加密算法：</strong> AES-GCM, AES-CBC, 和 AES-CTR。</li>
<li><strong>哈希函数：</strong> SHA-1, SHA-2, 和 SHA-3。</li>
<li><strong>密钥派生：</strong> HKDF 和 PBKDF2。</li>
<li><strong>认证机制：</strong> HMAC。</li>
<li><strong>密码学随机数生成器。</strong></li>
<li><strong>底层大整数和椭圆曲线实现</strong> (包括其精巧的汇编核心)。</li>
</ul>
<p>值得注意的是，像 TLS 和 X.509 这样的高层协议未在此次审计范围内。</p>
<p>Go 团队在博客中强调，他们对密码学包的安全性保障源于多方面的努力：</p>
<ol>
<li><strong>积极限制复杂性：</strong> 遵循“密码学原则”，例如优先考虑安全性而非极致性能。</li>
<li><strong>彻底的测试：</strong> 采用多种技术进行广泛测试。</li>
<li><strong>安全 API 的内部利用：</strong> 即使是内部包也倾向于使用安全的 API。</li>
<li><strong>利用 Go 语言特性：</strong> 避免常见的内存管理问题。</li>
<li><strong>注重可读性：</strong> 便于维护、代码审查和审计。</li>
</ol>
<h2>审计发现：低风险问题与建议性信息</h2>
<h3>一个低风险发现：Go+BoringCrypto 内存管理</h3>
<p>审计中唯一被标记为具有潜在可利用性的问题 (TOB-GOCL-3) 是一个<strong>低风险</strong>问题，影响小且难以触发。该问题涉及已废弃且不受支持的、基于 CGO 的 Go+BoringCrypto 集成中的内存管理。</p>
<p><strong>关键点：</strong></p>
<ul>
<li>此问题已在 <strong>Go 1.25 的开发分支中修复</strong>。</li>
<li>Go+BoringCrypto GOEXPERIMENT <strong>默认不启用</strong>，且<strong>不被 Go 团队支持在 Google 外部使用</strong>。</li>
<li>这个问题进一步坚定了 Go 团队<strong>转向原生 FIPS 140-3 模式</strong>的决心，该模式使用纯 Go 实现的密码学包，避免了 CGO 交互的复杂性和手动内存管理的风险。</li>
</ul>
<h3>五个建议性信息：与安全最佳实践息息相关</h3>
<p>其余五个发现本质上是<strong>建议性的 (informational)</strong>，不构成直接的安全风险，但与安全最佳实践相关。这些建议也已在 Go 1.25 开发分支中得到处理。</p>
<p>这些建议主要涉及：</p>
<ul>
<li><strong>潜在的计时侧信道 (Timing Side-Channels)：</strong> (TOB-GOCL-1, TOB-GOCL-2, TOB-GOCL-6)
<ul>
<li>crypto/ecdh, crypto/ecdsa: 字节到字段元素的转换非恒定时间。Go 团队决定将其改为恒定时间，以防未来被意外用于处理秘密值。</li>
<li>crypto/ecdsa: P-256 条件否定的 Power ISA 汇编实现非恒定时间 (CVE-2025-22866)。已与 IBM 合作修复。</li>
<li>crypto/ed25519: 标量内部外部表示转换非恒定时间。同样改为恒定时间。<br />
这些操作在现有用法中主要处理公开输入（如公钥），因此不被视为直接安全问题，但为了更强的鲁棒性和避免未来误用，Go 团队选择进行恒定时间修复。</li>
</ul>
</li>
<li><strong>内部 API 的误用风险：</strong> (TOB-GOCL-4)
<ul>
<li>crypto/internal/fips140/drbg: CTR_DRBG API 存在误用风险。由于此实现范围严格限定且未公开导出，Go 团队认为可接受，并通过文档警告明确了其限制。</li>
</ul>
</li>
<li><strong>实现完整性：</strong> (TOB-GOCL-5)
<ul>
<li>crypto/pbkdf2: 未强制执行 RFC 8018 中定义的输出长度限制。虽然实际中不太可能生成超长密钥（例如，使用 SHA-256 时超过 137GB），但为符合标准，此限制已被添加。</li>
</ul>
</li>
</ul>
<p>这些发现和修复再次体现了 Go 团队对密码学安全性的极致追求和透明度。</p>
<h2>Go 密码学的未来：PQC、FIPS 与更易用的 API</h2>
<p>审计结果令人鼓舞，但 Go 团队并未止步。他们正积极推进 Go 密码学库的现代化和易用性。</p>
<p><strong>1. 原生 FIPS 140-3 模式：</strong></p>
<p><strong>Go 1.24 已经包含了一个纯 Go 实现的 FIPS 140-3 模式</strong>，目前正在接受 CMVP (Cryptographic Module Validation Program) 测试。这将为所有 Go 用户提供一个受支持的、符合 FIPS 140-3 标准的密码学模式，取代之前不受支持的 Go+BoringCrypto 集成。</p>
<p><strong>2. 后量子密码学 (PQC) 的全面推进：</strong></p>
<p>正如我们之前讨论的，PQC 是应对未来量子计算机威胁的关键。Go 团队在这方面的工作取得了显著进展：</p>
<ul>
<li><strong>crypto/mlkem 包已在 Go 1.24 中正式引入：</strong> 实现了 ML-KEM-768 和 ML-KEM-1024，为开发者提供了直接使用后量子密钥封装机制的能力。</li>
<li><strong>crypto/tls 默认启用 X25519MLKEM768：</strong> <strong>Go 1.24 的一个重大更新！</strong> crypto/tls 包现在<strong>默认启用</strong>了 X25519MLKEM768 混合密钥交换机制（当 tls.Config.CurvePreferences 为 nil 时）。这意味着 Go 应用可以更轻松地获得针对经典和量子攻击的双重保护。开发者可以通过设置 CurvePreferences 或 GODEBUG=tlsmlkem=0 来控制此行为。</li>
</ul>
<p>以上两点在我的《<a href="https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go">未雨绸缪：Go开发者需要了解的后量子密码学与实现现状</a>》一文中有详细介绍。</p>
<p><strong>3. 更易用的高层密码学 API：</strong></p>
<p>Go 团队计划引入新的、更易用的高层密码学 API，旨在降低开发者选择和使用高质量加密算法的门槛。首个目标是要简化的密码哈希 API：** 计划提供一个简单的密码哈希 API，让用户无需纠结于选择众多可能的算法（如 bcrypt, scrypt, Argon2 等），并包含机制以在技术发展时自动迁移到更新的算法。这对于提升应用安全性至关重要。</p>
<h2>Go 与 PQC：机遇、挑战及开发者行动</h2>
<p>Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24已将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇。独立安全审计的积极结果进一步增强了社区对 Go 密码学库的信心。然而，PQC 技术的相对新颖性也带来了 API 演进、行业经验积累及潜在资源消耗（如密钥和签名尺寸增大）等方面的挑战。Go 社区正通过提供清晰文档、默认安全配置和推动行业实践来赋能开发者，共同塑造一个更安全的数字未来。</p>
<p>面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为（特别是 X25519MLKEM768 的默认启用）及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展（如对 ML-DSA 签名算法的支持）以及规划中的易用性 API（如密码哈希 API）。利用 Go 1.24 提供的坚实工具，并结合安全审计带来的信心，为应用的未来安全做好充分准备。</p>
<h2>小结：安全为本，Go 在密码学领域持续精进</h2>
<p>Go 官方对核心密码学库进行独立安全审计，并公开透明地分享结果和改进措施，再次彰显了其对安全性的坚定承诺。审计结果的积极性，结合 Go 1.24 在 PQC 和 FIPS 合规性方面的重大进展，无疑为 Go 开发者提供了更强大的信心和更先进的工具来构建安全的应用程序。</p>
<p>crypto/mlkem 的加入和 crypto/tls 中 X25519MLKEM768 的默认启用，标志着 Go 在后量子密码实用化方面迈出了重要一步。未来规划中更易用的高层密码学 API，如简化的密码哈希接口，将进一步降低安全开发的门槛，帮助开发者构建更加健壮和面向未来的系统。</p>
<p>这不仅是一系列技术更新，更是 Go 社区共同迈向更安全数字时代的体现。让我们持续关注 Go 在密码学领域的努力，积极采纳最佳实践，共同为构建一个能抵御未来威胁的数字世界贡献力量。</p>
<hr />
<p><strong>欢迎加入我的知识星球 “Go &amp; AI 精进营”！</strong></p>
<p>在这里，我们将一起：</p>
<ul>
<li>追踪 Go 官方在密码学和 PQC 领域的最新动态和深度解读。</li>
<li>分享和讨论 Go 安全编程的实战经验和代码示例。</li>
<li>探讨 PQC 算法选型、API 设计和性能优化。</li>
<li>构建一个积极、专业的 Go 安全技术交流社群。</li>
</ul>
<p>现在就扫描下方二维码加入星球，与更多 Gopher 一起，共同探索和构建更安全的 Go 应用，为量子未来做好准备！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>感谢您的阅读！</p>
<p>如果这篇文章让您对 Go 语言的密码学安全和 PQC 进展有了更清晰的认识，请不吝点赞、转发，让更多关注 Go 和信息安全的朋友们了解到这些重要动态！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/21/go-crypto-audit/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>未雨绸缪：Go开发者需要了解的后量子密码学与实现现状</title>
		<link>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/</link>
		<comments>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/#comments</comments>
		<pubDate>Tue, 20 May 2025 00:15:45 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Argon2]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[config]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[ECC]]></category>
		<category><![CDATA[ecdh]]></category>
		<category><![CDATA[FIPS-140]]></category>
		<category><![CDATA[fips-203]]></category>
		<category><![CDATA[FIPS-204]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.24]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[KEM]]></category>
		<category><![CDATA[mlkem]]></category>
		<category><![CDATA[NIST]]></category>
		<category><![CDATA[PQC]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[Shor]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[X25519MLKEM768]]></category>
		<category><![CDATA[后量子密码学]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[量子计算]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4730</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go 大家好，我是 Tony Bai。 在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——量子计算机。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。 这不是科幻电影，而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此，“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生，旨在研发能够抵御量子计算机攻击的新一代密码算法。 作为 Go 开发者，我们或许觉得量子计算机还很遥远，但“现在记录数据，未来量子破解”的风险已然存在。更重要的是，Go 语言作为一门以简洁、高效和安全著称的现代编程语言，其核心团队早已在为这个“后量子时代”积极布局。随着 Go 1.24 的发布，这一布局取得了实质性的进展：备受期待的 crypto/mlkem 包正式加入标准库！ 那么，PQC 究竟是什么？crypto/mlkem 包为我们带来了什么？Go 语言在 PQC 的浪潮中又将扮演怎样的角色？今天，就让我们一起“未雨绸缪”，深入了解 PQC 及其在 Go 中的最新进展。 量子风暴将至：为何我们需要 PQC？ 想象一下，你用 RSA 加密了公司的核心商业机密，或者用 ECDSA 签名了重要的合同。这些操作的安全性，都依赖于经典计算机难以在有效时间内解决某些数学难题（如大数分解、离散对数）。 然而，量子计算机一旦足够强大，Shor 算法就能在多项式时间内攻破这些难题。这意味着： 加密通讯不再私密： HTTPS、VPN 等都可能被破解。 数字签名不再可信： 软件更新、代码签名、身份认证都可能被伪造。 历史数据面临风险： 黑客现在就可以截获并存储加密数据，等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等，这无疑是巨大威胁。 这就是我们迫切需要 PQC 的原因：寻找并标准化那些即使是量子计算机也难以破解的新密码算法。 PQC 的曙光：NIST 标准化与主流算法 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/post-quantum-cryptography-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go">本文永久链接</a> &#8211; https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go</p>
<p>大家好，我是 Tony Bai。</p>
<p>在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——<strong>量子计算机</strong>。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。</p>
<p>这不是科幻电影，而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此，“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生，旨在研发能够抵御量子计算机攻击的新一代密码算法。</p>
<p>作为 Go 开发者，我们或许觉得量子计算机还很遥远，但“现在记录数据，未来量子破解”的风险已然存在。更重要的是，Go 语言作为一门以简洁、高效和安全著称的现代编程语言，其核心团队早已在为这个“后量子时代”积极布局。<strong>随着 Go 1.24 的发布，这一布局取得了实质性的进展：备受期待的 crypto/mlkem 包正式加入标准库！</strong></p>
<p>那么，PQC 究竟是什么？crypto/mlkem 包为我们带来了什么？Go 语言在 PQC 的浪潮中又将扮演怎样的角色？今天，就让我们一起“未雨绸缪”，深入了解 PQC 及其在 Go 中的最新进展。</p>
<p><img src="https://tonybai.com/wp-content/uploads/course-card/go-advanced-course-4.png" alt="" /></p>
<h2>量子风暴将至：为何我们需要 PQC？</h2>
<p>想象一下，你用 RSA 加密了公司的核心商业机密，或者用 ECDSA 签名了重要的合同。这些操作的安全性，都依赖于经典计算机难以在有效时间内解决某些数学难题（如大数分解、离散对数）。</p>
<p>然而，量子计算机一旦足够强大，<a href="https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go">Shor 算法</a>就能在多项式时间内攻破这些难题。这意味着：</p>
<ul>
<li><strong>加密通讯不再私密：</strong> HTTPS、VPN 等都可能被破解。</li>
<li><strong>数字签名不再可信：</strong> 软件更新、代码签名、身份认证都可能被伪造。</li>
<li><strong>历史数据面临风险：</strong> 黑客现在就可以截获并存储加密数据，等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等，这无疑是巨大威胁。</li>
</ul>
<p>这就是我们迫切需要 PQC 的原因：<strong>寻找并标准化那些即使是量子计算机也难以破解的新密码算法。</strong></p>
<h2>PQC 的曙光：NIST 标准化与主流算法</h2>
<p>幸运的是，我们并非束手无策。在应对后量子密码学（PQC）的挑战时，美国国家标准与技术研究院（NIST）自2016年启动了PQ算法的标准化进程，旨在筛选和确立新一代的密码算法。经过多轮评审，几种优胜算法逐渐浮出水面，为未来的安全通信提供了希望。</p>
<p>首先，在密钥封装/交换机制（KEM &#8211; Key Encapsulation Mechanism)方面，基于格密码学（Lattice-based cryptography）的ML-KEM被选为主要的KEM标准（FIPS 203）。这一算法的优势在于，某些实现的性能甚至超过了我们熟知的X25519密钥交换算法，为其广泛应用奠定了基础。</p>
<p>简单来说，KEM的工作原理可以类比于使用一个特殊的“量子安全信封”——公钥，将对称密钥（例如AES密钥）封装后发送给对方。接收方使用对应的“量子安全钥匙”——私钥，打开信封取出密钥，随后双方便可借助这个对称密钥进行安全的通信。</p>
<p>在数字签名方面，ML-DSA基于Dilithium算法，同样属于格密码学的范畴。该算法被选为主要的数字签名标准（FIPS 204），用于验证信息的来源及完整性。这为数字通信的安全性提供了重要保障。</p>
<p>通过这些新兴的密码算法，NIST为抵御未来的量子攻击奠定了坚实的基础，展现了在后量子时代中应对安全挑战的希望。</p>
<p><strong>PQC 算法的挑战：更大的“块头”</strong></p>
<p>虽然 PQC 算法带来了量子抵抗性，但也普遍面临一个挑战：<strong>密钥和签名的尺寸通常比经典算法大得多。</strong> 这可能会对网络带宽、存储空间（尤其是 X.509 证书）以及资源受限设备带来一定压力。</p>
<p><strong>现有算法与 PQC 替换畅想 (简表):</strong></p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/post-quantum-cryptography-in-go-2.png" alt="" /></p>
<blockquote>
<p>注意：上表为简化对应，实际替换过程会更复杂。</p>
</blockquote>
<h2>Go语言与PQC：Go 1.24 迎来 crypto/mlkem</h2>
<p>Go 语言以其强大的 crypto 标准库和对安全性的重视而闻名。面对 PQC 的浪潮，Go 核心团队自然不会缺席。他们的策略是<strong>谨慎、务实且前瞻</strong>。</p>
<h3>Go 密码学库的坚实基础</h3>
<p>在讨论 PQC 之前，值得一提的是 Go 现有密码学库的优秀设计：</p>
<ul>
<li><strong>简洁易用：</strong> 尽量减少复杂性，提供安全的默认值，降低开发者误用风险。</li>
<li><strong>持续现代化：</strong> 如对 RSA 后端的优化、新增 crypto/ecdh 包简化密钥交换、通过 godebug 机制平滑引入安全改进等。</li>
<li><strong>golang.org/x/crypto：</strong> 作为标准库的扩展和试验田，引入 ChaCha20、Argon2 等高级算法。</li>
</ul>
<p>在 crypto/mlkem 正式发布之前，Go 团队已在早期版本（如 Go 1.23）中进行了内部实现和集成工作。这些工作为标准库的最终引入奠定了基础，尤其是在 crypto/tls 包中探索对后量子密钥交换的支持。</p>
<h3>Go 1.24：crypto/mlkem 包正式发布！</h3>
<p><strong>激动人心的时刻终于到来！Go 1.24 版本正式将 crypto/mlkem 包引入标准库。</strong> 这一里程碑事件由 Go 核心开发者 FiloSottile（Filippo Valsorda）在 <a href="https://github.com/golang/go/issues/70122">Go Issue #70122</a> 中提议并推动实现。</p>
<p>crypto/mlkem 包实现了 FIPS 203 标准中定义的 ML-KEM 算法，目前支持以下两个参数集：</p>
<ul>
<li><strong>ML-KEM-768:</strong> 这是在大多数场景中推荐使用的参数集，提供了足够的后量子安全性。</li>
<li><strong>ML-KEM-1024:</strong> 主要用于满足 CNSA 2.0 等特定规范的要求。</li>
</ul>
<p>Go 团队暂时未包含 ML-KEM-512，因为它在实际部署中较为罕见。这一选择与 BoringSSL 等其他主流密码库的实现保持一致。</p>
<p>下面是crypto/mlkem 包 API的一些设计考量：</p>
<ul>
<li><strong>类型安全：</strong> 为 ML-KEM-768 和 ML-KEM-1024 提供了独立的类型（如 mlkem.DecapsulationKey768 和 mlkem.EncapsulationKey768），避免了因参数集不同导致数据结构大小不匹配的问题。虽然数字后缀在 godoc 中的排序可能不理想，但这是为了类型清晰和性能考虑的权衡。</li>
<li><strong>种子作为解封装密钥：</strong> 解封装密钥（私钥）支持以 64 字节的种子（”d || z” 形式）进行创建和表示。这种格式与 IETF 的方向一致，有助于标准化和互操作性。</li>
<li><strong>简洁的核心操作：</strong> API 围绕密钥生成 (GenerateKey)、密钥解析 (NewDecapsulationKey/NewEncapsulationKey)、密钥字节化 (Bytes)、封装 (Encapsulate) 和解封装 (Decapsulate) 这几个核心 KEM 操作展开。</li>
</ul>
<h3>crypto/mlkem 使用示例</h3>
<p>下面是一个演示如何使用 crypto/mlkem 包进行密钥封装和解封装的基本流程：</p>
<pre><code class="go">package main

import (
    "bytes"
    "crypto/mlkem"
    "fmt"
    "log"
)

func main() {
    // === 场景：Alice 希望与 Bob 安全地共享一个密钥 ===

    // --- Alice 的操作 ---
    // 1. Alice 生成一个 ML-KEM-768 私钥 (DecapsulationKey)。
    //    GenerateKey768 返回 (*DecapsulationKey768, error)。
    privateKeyAlice, err := mlkem.GenerateKey768()
    if err != nil {
        log.Fatalf("Alice: Failed to generate ML-KEM-768 decapsulation key: %v", err)
    }

    // 2. 从私钥获取对应的公钥 (EncapsulationKey)。
    publicKeyAlice := privateKeyAlice.EncapsulationKey()

    // 3. Alice 将她的公钥序列化为字节串，以便发送给 Bob。
    publicKeyAliceBytes := publicKeyAlice.Bytes()
    fmt.Printf("Alice's Public Key (ML-KEM-768, %d bytes): %x...\n", len(publicKeyAliceBytes), publicKeyAliceBytes[:16])

    // --- Bob 的操作 ---
    // Bob 接收到 Alice 的公钥字节串 publicKeyAliceBytes

    // 4. Bob 根据接收到的字节串创建一个公钥实例。
    //    NewEncapsulationKey768 返回 (*EncapsulationKey768, error)。
    publicKeyReceivedByBob, err := mlkem.NewEncapsulationKey768(publicKeyAliceBytes)
    if err != nil {
        log.Fatalf("Bob: Failed to parse Alice's public key: %v", err)
    }

    // 5. Bob 使用 Alice 的公钥来封装一个新的共享密钥。
    //    Encapsulate 返回 (sharedKey []byte, ciphertext []byte)，不返回 error。
    sharedKeyForBob, ciphertextForAlice := publicKeyReceivedByBob.Encapsulate()
    fmt.Printf("Bob: Generated Shared Key (ML-KEM-768, %d bytes): %x\n", len(sharedKeyForBob), sharedKeyForBob)
    fmt.Printf("Bob: Generated Ciphertext for Alice (%d bytes): %x...\n", len(ciphertextForAlice), ciphertextForAlice[:16])

    // --- Alice 的操作 ---
    // Alice 接收到 Bob 发送过来的密文 ciphertextForAlice

    // 6. Alice 使用她的私钥和收到的密文来解封装，得到共享密钥。
    //    Decapsulate 返回 (sharedKey []byte, error)。
    sharedKeyForAlice, err := privateKeyAlice.Decapsulate(ciphertextForAlice)
    if err != nil {
        // 如果密文无效或已被篡改，Decapsulate 会返回错误。
        log.Fatalf("Alice: Failed to decapsulate shared key: %v", err)
    }
    fmt.Printf("Alice: Decapsulated Shared Key (ML-KEM-768, %d bytes): %x\n", len(sharedKeyForAlice), sharedKeyForAlice)

    // --- 验证 ---
    // 7. 验证 Alice 和 Bob 得到的共享密钥是否一致。
    if bytes.Equal(sharedKeyForAlice, sharedKeyForBob) {
        fmt.Println("\nSuccess! Alice and Bob now share the same secret key using ML-KEM-768.")
    } else {
        // 这通常不应该发生，如果 Decapsulate 成功且数据未被篡改。
        fmt.Println("\nError! Shared keys do NOT match. This is unexpected.")
    }

    // 简单演示 ML-KEM-1024 的密钥生成 (API 结构类似)
    dk1024, err := mlkem.GenerateKey1024()
    if err != nil {
        log.Fatalf("Failed to generate ML-KEM-1024 decapsulation key: %v", err)
    }
    _ = dk1024.EncapsulationKey() // 获取公钥
    fmt.Println("\nSuccessfully demonstrated ML-KEM-1024 key generation as well.")

    // 打印一些常量信息
    fmt.Printf("\nML-KEM Constants:\n")
    fmt.Printf("  SharedKeySize: %d bytes\n", mlkem.SharedKeySize)
    fmt.Printf("  SeedSize: %d bytes\n", mlkem.SeedSize)
    fmt.Printf("  CiphertextSize768: %d bytes\n", mlkem.CiphertextSize768)
    fmt.Printf("  EncapsulationKeySize768: %d bytes\n", mlkem.EncapsulationKeySize768)
    fmt.Printf("  CiphertextSize1024: %d bytes\n", mlkem.CiphertextSize1024)
    fmt.Printf("  EncapsulationKeySize1024: %d bytes\n", mlkem.EncapsulationKeySize1024)
}
</code></pre>
<p>使用Go 1.24+版本运行上述代码，可以得到类似如下输出结果：</p>
<pre><code>Alice's Public Key (ML-KEM-768, 1184 bytes): f880089a159c9ba338a684c70e10bdee...
Bob: Generated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f
Bob: Generated Ciphertext for Alice (1088 bytes): 9afe9f9d36a581a5d7e47b7913c65886...
Alice: Decapsulated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f

Success! Alice and Bob now share the same secret key using ML-KEM-768.

Successfully demonstrated ML-KEM-1024 key generation as well.

ML-KEM Constants:
  SharedKeySize: 32 bytes
  SeedSize: 64 bytes
  CiphertextSize768: 1088 bytes
  EncapsulationKeySize768: 1184 bytes
  CiphertextSize1024: 1568 bytes
  EncapsulationKeySize1024: 1568 bytes
</code></pre>
<p>上述代码仅为了展示 crypto/mlkem 包的核心用法。实际应用中，公钥和密文的传输需要通过网络等信道。</p>
<p>crypto/mlkem 包的加入，使得 Go 开发者可以直接在应用层使用标准化的后量子密钥封装机制，为构建面向未来的安全应用提供了坚实的基础。</p>
<h3>crypto/tls 的 PQC 集成：Go 1.24 默认启用，让 HTTPS 更“抗量子”</h3>
<p>对于大多数 Go 开发者而言，直接使用底层的 crypto/mlkem 包可能不是最常见的场景。更令人振奋的是，Go 团队已将后量子密码能力无缝集成到了我们日常使用的 crypto/tls 包中！</p>
<p><strong>根据 Go 1.24 的发布说明，crypto/tls 包现在默认支持并启用了新的后量子混合密钥交换机制 X25519MLKEM768。</strong></p>
<p>这意味着什么呢？</p>
<ul>
<li><strong>默认的后量子保护：</strong> 当你的 Go 1.24+ 应用程序使用 crypto/tls（例如，作为 HTTPS 服务器或客户端），并且 tls.Config 中的 CurvePreferences 字段未被显式设置（保持为 nil）时，TLS 握手将<strong>自动尝试使用 X25519MLKEM768 进行密钥交换</strong>。</li>
<li><strong>混合机制的优势：</strong> X25519MLKEM768 是一种<strong>混合 (hybrid)</strong> 密钥交换方案。它巧妙地将经过广泛验证的经典椭圆曲线算法 X25519 与后量子安全的 ML-KEM-768 结合起来。这样做的好处是：
<ul>
<li><strong>经典安全：</strong> 即使 ML-KEM-768 未来被发现存在未知的弱点（尽管可能性很小），X25519 依然能提供经典的安全性。</li>
<li><strong>量子抵抗：</strong> 面对量子计算机的威胁，ML-KEM-768 部分提供了后量子保护。</li>
</ul>
</li>
</ul>
<p>这种“两全其美”的设计是当前 PQC 过渡阶段推荐的主流方案。</p>
<ul>
<li><strong>开发者体验的极致简化：</strong> 大部分情况下，开发者<strong>无需修改现有代码</strong>即可获得这种增强的安全性。Go 语言的哲学再次体现——将复杂性封装起来，提供安全且易用的默认行为。</li>
</ul>
<p><strong>如何控制这一行为？</strong></p>
<p>虽然默认启用是推荐的，但 Go 团队也考虑到了现实世界中的兼容性问题。在某些情况下，一些老旧或有缺陷的 TLS 服务器可能无法正确处理 X25519MLKEM768 握手过程中可能产生的较大记录 (TLS record)，导致握手超时失败。</p>
<p>因此，Go 1.24 提供了禁用此默认行为的选项：</p>
<ul>
<li><strong>通过 tls.Config.CurvePreferences：</strong></li>
</ul>
<p>你可以显式设置 CurvePreferences 字段，只包含你希望支持的经典密钥交换机制，从而排除 X25519MLKEM768。</p>
<pre><code class="go">// 示例：显式设置，不包含 X25519MLKEM768
config := &amp;tls.Config{
    Certificates:     []tls.Certificate{cert},
    CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, // 明确指定经典曲线
}
</code></pre>
<ul>
<li><strong>通过 GODEBUG 环境变量：</strong></li>
</ul>
<p>可以在运行时通过设置环境变量 GODEBUG=tlsmlkem=0 来全局禁用 X25519MLKEM768 的默认启用。</p>
<p><strong>概念性代码示例（体现 Go 1.24 默认行为）：</strong></p>
<pre><code class="go">package main

import (
    "crypto/tls"
    "log"
    "net/http"
)

func helloServer(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server with Go 1.24 TLS.\n"))
    // Go 1.24 crypto/tls 默认会尝试 X25519MLKEM768 密钥交换
    // 如果客户端也支持，连接将具备后量子安全性！
}

func main() {
    // 加载证书和私钥 (此处省略具体加载过程)
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        log.Fatalf("server: loadkeys: %s", err)
    }

    // 服务器配置
    // 在 Go 1.24+ 中，如果 CurvePreferences 为 nil (默认)，
    // X25519MLKEM768 将被默认启用。
    config := &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
        // CurvePreferences: nil, // 默认即启用 X25519MLKEM768
    }

    http.HandleFunc("/hello", helloServer)
    server := &amp;http.Server{
        Addr:      ":8443",
        TLSConfig: config,
    }
    log.Println("Starting server on https://localhost:8443/hello")
    log.Fatal(server.ListenAndServeTLS("", "")) // 使用空字符串让其加载 config 中的证书
}

// 客户端示例 (概念)
// clientConfig := &amp;tls.Config{
// InsecureSkipVerify: true, // 仅用于测试，生产环境不要用
//     CurvePreferences: nil, // 客户端也会默认尝试 X25519MLKEM768
// }
// conn, err := tls.Dial("tcp", "localhost:8443", clientConfig)
// if err != nil {
//     log.Fatalf("client: dial: %s", err)
// }
// defer conn.Close()
// log.Println("client: connected to: ", conn.RemoteAddr())
</code></pre>
<p><strong>重要提示：</strong> Go 1.24 还<strong>移除了对实验性 X25519Kyber768Draft00 密钥交换的支持</strong>，完全转向了标准化的 X25519MLKEM768。</p>
<p>crypto/tls 中这一默认的后量子安全增强，是 Go 语言在 PQC 时代向前迈出的坚实一步，极大地降低了开发者应用 PQC 的门槛。</p>
<p>开发者可能只需要更新 Go 版本，或者做少量配置，就能让应用具备初步的后量子防护能力，而无需深入了解 ML-KEM 的复杂细节。这正是 Go 追求简洁易用哲学的体现。</p>
<p>对于 SSH 协议，Go 团队计划密切关注 OpenSSH 的发展。一旦 OpenSSH 支持 NIST 选定的 ML-KEM 标准（目前 OpenSSH 使用的是 NTRU，非 NIST 主选），Go 团队也将在 crypto/ssh 包中添加相应支持，以确保互操作性。</p>
<h2>Go 与 PQC：机遇、挑战及开发者行动</h2>
<p>Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24 将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇，但也伴随着 API 演进、行业经验不足及资源消耗等挑战。Go 社区正通过提供清晰文档和推动实践来赋能开发者，共同塑造一个更安全的数字未来。</p>
<p>面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展，并参与社区交流，为应用的未来安全做好规划。Go 1.24 已为我们迈向后量子安全提供了坚实的工具。</p>
<h2>小结：为量子未来，Go 已在路上</h2>
<p>后量子密码学不再是遥不可及的未来概念，而是关乎我们数字世界长期安全的关键一步。Go 语言凭借其在密码学领域的深厚积累和前瞻性布局，正稳步迈向这个新时代。</p>
<p>Go 1.24 中 crypto/mlkem 包的正式发布，是 Go 在 PQC 领域的一个重要里程碑。它为开发者提供了直接、标准化的工具来应对量子计算的潜在威胁。结合未来在 crypto/tls 和 crypto/ssh 等包中的 PQC 集成，Go 团队正努力为开发者提供安全、易用且高效的后量子密码解决方案。</p>
<p>这不仅是一场技术升级，更是 Go 社区共同承担的责任。通过学习、关注、参与和实践，我们可以与 Go 一道，为构建一个能抵御未来量子威胁的、更安全的数字世界贡献力量。让我们一起期待 Go 在后量子时代的精彩表现！</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/issues/70122">crypto/mlkem: new package #70122</a> &#8211; https://github.com/golang/go/issues/70122</li>
<li><a href="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards">NIST Releases First 3 Finalized Post-Quantum Encryption Standards</a> &#8211; https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards </li>
<li><a href="https://csrc.nist.gov/projects/post-quantum-cryptography">Post-Quantum Cryptography</a> &#8211; https://csrc.nist.gov/projects/post-quantum-cryptography</li>
<li><a href="https://en.wikipedia.org/wiki/Post-quantum_cryptography">Post-quantum cryptography</a> &#8211; https://en.wikipedia.org/wiki/Post-quantum_cryptography</li>
<li><a href="https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/">Towards Post-Quantum Cryptography in TLS</a> &#8211; https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/</li>
<li><a href="https://words.filippo.io/dispatches/post-quantum-age/">KEMS AND POST-QUANTUM AGE</a> &#8211; https://words.filippo.io/dispatches/post-quantum-age/ </li>
<li><a href="https://words.filippo.io/dispatches/mlkem768/">POST-QUANTUM CRYPTOGRAPHY FOR THE GO ECOSYSTEM</a> &#8211; https://words.filippo.io/dispatches/mlkem768/</li>
<li><a href="https://changelog.com/gotime/295?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 1</a> &#8211; https://changelog.com/gotime/295</li>
<li><a href="https://changelog.com/gotime/298?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 2</a> &#8211; https://changelog.com/gotime/298</li>
<li><a href="https://changelog.com/gotime/313?ref=tonybai.com">What&#8217;s new in Go&#8217;s cryptography libraries: Part 3</a> &#8211; https://changelog.com/gotime/313</li>
<li><a href="https://medium.com/cyberark-engineering/a-post-quantum-cryptography-web-server-in-go-1-23-9f7e98db7b39">Post Quantum Cryptography Web Server in Go 1.23</a> &#8211; https://medium.com/cyberark-engineering/a-post-quantum-cryptography-web-server-in-go-1-23-9f7e98db7b39</li>
</ul>
<hr />
<p><strong>感谢阅读！</strong></p>
<p>如果这篇文章让你对后量子密码学和 Go 的未来有所了解，请帮忙<strong>转发</strong> ，让更多 Gopher 关注这一重要趋势！</p>
<hr />
<p><strong>深入探讨，加入我们！</strong></p>
<p>对后量子密码学的技术细节、Go 的具体实现进展、或如何在你的应用中规划 PQC 过渡感兴趣？欢迎加入我的知识星球 <strong>“Go &amp; AI 精进营”</strong>！</p>
<p>在那里，我们可以：</p>
<ul>
<li>跟踪 PQC 在 Go 及其他主流语言中的最新动态。</li>
<li>讨论 PQC API 设计的最佳实践。</li>
<li>分享学习 PQC 和密码学的心得体会。</li>
</ul>
<p>欢迎扫描下方二维码加入星球，共同探索安全技术的未来！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？</title>
		<link>https://tonybai.com/2025/05/16/energy-savings-if-abandon-https/</link>
		<comments>https://tonybai.com/2025/05/16/energy-savings-if-abandon-https/#comments</comments>
		<pubDate>Fri, 16 May 2025 14:06:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[CDN]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[Diffie-Hellman]]></category>
		<category><![CDATA[ECC]]></category>
		<category><![CDATA[handshake]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[MAC]]></category>
		<category><![CDATA[padding]]></category>
		<category><![CDATA[Programmar]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[RTT]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[TLS1.2]]></category>
		<category><![CDATA[TLS1.3]]></category>
		<category><![CDATA[会话复用]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[对称加密]]></category>
		<category><![CDATA[数据中心]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[椭圆曲线]]></category>
		<category><![CDATA[消息认证码]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[算力]]></category>
		<category><![CDATA[身份认证]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4718</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/16/energy-savings-if-abandon-https 大家好，我是Tony Bai。 如今，当我们浏览网页时，地址栏那把绿色的小锁和 HTTPS 前缀已是司空见惯。从网上银行到个人博客，再到每一个SaaS服务，HTTPS/TLS 加密几乎覆盖了互联网的每一个角落。它像一位忠诚的数字保镖，守护着我们在虚拟世界中的数据安全与隐私。 然而，这位保镖并非“免费服务”。HTTPS/TLS 在带来安全的同时，也无可避免地引入了额外的计算和传输开销，直观感受便是连接速度可能略有减慢，传输数据量也略有增加。而且，随着我们对安全的追求永无止境，为了抵御更强大的计算破解能力，加密算法的密钥长度也在不断增加（例如从 RSA 1024位到2048位甚至更高，ECC 曲线的复杂度也在提升），这无疑进一步加剧了这些开销。 那么，今天我们不妨来做一个大胆的，甚至有些“异想天开”的思想实验：如果在一夜之间，全球所有的网站都决定弃用 HTTPS/TLS，回归到“裸奔”的 HTTP 时代，理论上能为我们的地球节省多少电力呢？ 重要声明： 这纯粹是一个思想实验，旨在通过一个极端的假设，引发我们对技术成本（特别是能源成本）和安全效益之间平衡的思考。我们绝非鼓吹放弃 HTTPS/TLS，其在现代互联网安全中的基石地位无可替代。 HTTPS 的“能源账单”：开销源自何方？ 要估算节省的电量，首先得理解 HTTPS/TLS 的主要开销在哪里。这些开销主要体现在两个方面：计算开销和数据传输开销。 计算开销 (CPU 的额外负担) TLS 握手阶段： 这是计算密集型操作的重灾区。 非对称加密/密钥交换： 如 RSA、Diffie-Hellman 或 ECC (椭圆曲线加密)，用于安全地协商后续通信所用的对称密钥。密钥长度的增加，使得这些运算的计算量呈指数级或更高阶的增长。 例如，一个 RSA 2048 位操作的计算量远超 1024 位。 证书验证： 客户端需要验证服务器证书链的有效性，这涉及到一系列的数字签名验证操作，同样消耗 CPU 资源。 对称密钥生成与哈希计算： 用于生成会话密钥、消息认证码 (MAC) 等。 数据传输阶段： 对称加解密： 建立连接后，所有应用数据的传输都需要经过对称加密算法（如 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/energy-savings-if-abandon-https-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/16/energy-savings-if-abandon-https">本文永久链接</a> &#8211; https://tonybai.com/2025/05/16/energy-savings-if-abandon-https</p>
<p>大家好，我是Tony Bai。</p>
<p>如今，当我们浏览网页时，地址栏那把绿色的小锁和 HTTPS 前缀已是司空见惯。从网上银行到个人博客，再到每一个SaaS服务，HTTPS/TLS 加密几乎覆盖了互联网的每一个角落。它像一位忠诚的数字保镖，守护着我们在虚拟世界中的数据安全与隐私。</p>
<p>然而，这位保镖并非“免费服务”。HTTPS/TLS 在带来安全的同时，也无可避免地引入了额外的计算和传输开销，直观感受便是连接速度可能略有减慢，传输数据量也略有增加。而且，随着我们对安全的追求永无止境，为了抵御更强大的计算破解能力，<strong>加密算法的密钥长度也在不断增加</strong>（例如从 RSA 1024位到2048位甚至更高，ECC 曲线的复杂度也在提升），这无疑进一步加剧了这些开销。</p>
<p>那么，今天我们不妨来做一个大胆的，甚至有些“异想天开”的<strong>思想实验</strong>：<strong>如果在一夜之间，全球所有的网站都决定弃用 HTTPS/TLS，回归到“裸奔”的 HTTP 时代，理论上能为我们的地球节省多少电力呢？</strong></p>
<p><strong>重要声明：</strong> 这纯粹是一个思想实验，旨在通过一个极端的假设，引发我们对技术成本（特别是能源成本）和安全效益之间平衡的思考。<strong>我们绝非鼓吹放弃 HTTPS/TLS，其在现代互联网安全中的基石地位无可替代。</strong></p>
<h2>HTTPS 的“能源账单”：开销源自何方？</h2>
<p><img src="https://tonybai.com/wp-content/uploads/2025/energy-savings-if-abandon-https-2.png" alt="示意图来自bytebytego" /></p>
<p>要估算节省的电量，首先得理解 HTTPS/TLS 的主要开销在哪里。这些开销主要体现在两个方面：<strong>计算开销</strong>和<strong>数据传输开销</strong>。</p>
<h3>计算开销 (CPU 的额外负担)</h3>
<ul>
<li><strong>TLS 握手阶段：</strong> 这是计算密集型操作的重灾区。
<ul>
<li><strong>非对称加密/密钥交换：</strong> 如 RSA、Diffie-Hellman 或 ECC (椭圆曲线加密)，用于安全地协商后续通信所用的对称密钥。<strong>密钥长度的增加，使得这些运算的计算量呈指数级或更高阶的增长。</strong> 例如，一个 RSA 2048 位操作的计算量远超 1024 位。</li>
<li><strong>证书验证：</strong> 客户端需要验证服务器证书链的有效性，这涉及到一系列的数字签名验证操作，同样消耗 CPU 资源。</li>
<li><strong>对称密钥生成与哈希计算：</strong> 用于生成会话密钥、消息认证码 (MAC) 等。</li>
</ul>
</li>
<li><strong>数据传输阶段：</strong>
<ul>
<li><strong>对称加解密：</strong> 建立连接后，所有应用数据的传输都需要经过对称加密算法（如 AES）的加密和解密。虽然对称加密比非对称加密快得多，但对于海量数据流，累积的 CPU 开销依然可观。</li>
<li><strong>消息认证码 (MAC) 计算：</strong> 为确保数据完整性，需要为每个数据包计算和验证 MAC。</li>
</ul>
</li>
</ul>
<p>这些计算开销不仅发生在服务器端（数据中心），也发生在每一个发起 HTTPS 请求的客户端设备上（我们的电脑、手机等）。</p>
<h3>数据传输开销 (网络带宽的额外占用)</h3>
<ul>
<li><strong>TLS 握手数据包：</strong> 完整的 TLS 握手过程（尤其是在未使用会话复用或 TLS 1.3 的 0-RTT 时）需要多个数据包的往返，这些数据包承载了证书、加密套件协商信息、密钥交换参数等，本身就构成了额外的网络流量。</li>
<li><strong>TLS 记录层头部：</strong> 每个 TLS 记录包都会增加一个小的头部，指明内容类型、版本和长度。</li>
<li><strong>填充数据 (Padding)：</strong> 某些块加密模式可能需要填充数据以满足块大小要求。</li>
</ul>
<p>这些额外的字节虽然对单个请求来说可能不多，但考虑到全球互联网的流量规模，累积起来也是一个惊人的数字。这些额外的数据不仅消耗了网络设备（路由器、交换机、基站）的传输和处理电力，也增加了数据中心内部的存储和带宽压力。</p>
<h2>尝试量化：一个极度简化的估算</h2>
<p>精确计算全球弃用 HTTPS 能节省多少电量几乎是不可能的，因为这涉及到太多动态和难以获取的数据。但我们可以尝试进行一个基于合理假设的粗略数量级估算，目的在于理解其可能的影响范围。</p>
<p><strong>请注意：以下估算高度简化，仅为引发思考，不代表任何精确的科学结论。</strong></p>
<ul>
<li><strong>假设一：全球每日 HTTPS 请求数。</strong> 据一些行业报告估计，全球每日的 HTTP(S) 请求量可能达到数百万亿甚至更高。我们不妨取一个相对保守的中间值。</li>
<li><strong>假设二：单次 TLS 握手与数据加解密的平均额外能耗。</strong> 这取决于多种因素，包括密钥长度、加密算法、硬件加速能力等。我们可以参考一些研究中关于 CPU 执行加密操作的功耗数据，或者服务器因处理 TLS 产生的额外负载百分比。</li>
<li><strong>假设三：TLS 协议的平均数据开销。</strong> TLS 握手通常会增加几KB的开销，后续记录层头部等开销相对较小，我们可以估算一个平均的额外数据传输百分比。</li>
<li><strong>假设四：全球数据中心和网络基础设施的总能耗。</strong> 这同样是一个巨大的数字，数据中心本身就是能源消耗大户。</li>
</ul>
<p>基于这些高度简化的假设，即使我们只考虑由于 TLS 计算和额外数据传输导致的 <strong>全球数据中心电力消耗增加 1%-5%</strong> （这已经是一个非常大胆且可能偏低的估计，因为 TLS 的影响是端到端的），考虑到全球数据中心年耗电量已达数百太瓦时 (TWh，1太瓦时=10亿度电) 的量级，这意味着：</p>
<p><strong>理论上，弃用 HTTPS 每年节省的电力可能达到数个乃至数十个太瓦时。</strong></p>
<p>这是什么概念？一个太瓦时的电力，足以供应数十万个普通家庭一年的用电。数十太瓦时，其能源足迹和碳排放影响将是巨大的。</p>
<p>再次强调，这只是一个非常粗略的“思想实验”级别估算。实际情况远比这复杂，例如：</p>
<ul>
<li>现代 CPU 对 AES 等对称加密有硬件指令加速，大大降低了数据传输阶段的加密开销。</li>
<li>TLS 1.3 显著优化了握手过程，减少了 RTT 和计算量。</li>
<li>会话复用技术能避免重复的完整握手。</li>
<li>CDN 和边缘节点分担了部分 TLS 终结的压力。</li>
</ul>
<p>但即便如此，考虑到<strong>密钥长度持续增加带来的计算压力</strong>，以及全球网络流量的爆炸式增长，HTTPS/TLS 的“能源税”依然是一个不容忽视的议题。</p>
<h2>安全的代价：我们为何“心甘情愿”支付这笔账单？</h2>
<p>既然 HTTPS/TLS 有如此“隐形”的能源成本，为何我们还要坚定不移地推动全网 HTTPS 化呢？</p>
<p>答案不言而喻：<strong>安全！</strong></p>
<ul>
<li><strong>数据保密性：</strong> 防止敏感信息（如登录凭证、支付信息、个人隐私）在传输过程中被窃听。</li>
<li><strong>数据完整性：</strong> 确保数据在传输过程中未被篡改。</li>
<li><strong>身份认证：</strong> 验证通信对方（主要是服务器）的真实身份，防止中间人攻击。</li>
</ul>
<p>在一个充斥着网络钓鱼、数据泄露、恶意劫持的数字时代，这些安全保障是我们进行在线活动的基础信任。<strong>与可能遭受的经济损失、声誉损害、隐私侵犯相比，HTTPS/TLS 的能源成本可以说是“必要的代价”。</strong></p>
<h2>追求平衡：我们能为“绿色安全”做些什么？</h2>
<p>这次思想实验的目的，绝非要我们因噎废食，放弃安全。恰恰相反，它应该促使我们更积极地思考：<strong>如何在保障同等级别安全的前提下，追求更高的效率和更低的能耗？</strong></p>
<ol>
<li><strong>持续优化协议与算法：</strong> TLS 1.3 就是一个很好的例子(Go标准库crypto/tls已经默认采用TLS 1.3)。未来是否还会有更轻量级、更高性能的安全协议或加密算法出现？</li>
<li><strong>硬件加速的普及：</strong> 推动和利用 CPU、专用加密芯片对加密运算的硬件加速能力。</li>
<li><strong>智能的会话管理：</strong> 更有效地利用会话复用、0-RTT 等技术，减少不必要的握手开销。</li>
<li><strong>内容分发与边缘计算的优化：</strong> 在离用户更近的地方进行 TLS 终结，减少长距离加密传输的开销。</li>
<li><strong>代码层面的优化：</strong> 对于应用开发者，合理设计 API，避免不必要的加密数据传输，选择合适的加密库和配置。</li>
<li><strong>关注“适度安全”：</strong> 对于某些内部系统或低风险场景，是否可以采用与公网不同强度的、但依然安全的加密策略？（这需要非常谨慎的评估）。</li>
</ol>
<h2>小结：思想实验的价值在于警醒与前瞻</h2>
<p>“如果全球网站弃用 HTTPS，能为地球节省多少电？” 这个问题的答案可能永远无法精确计算，但它像一面镜子，照见了我们为构建一个更安全的数字世界所付出的“隐形成本”之一。</p>
<p>这提醒我们，<strong>安全并非没有代价，技术进步需要在多个维度上寻求平衡。</strong> 在坚定不移地拥抱和强化网络安全的同时，我们也应该持续关注其对性能、资源和环境的影响，积极探索和实践更绿色、更高效的安全技术。</p>
<hr />
<p><strong>聊一聊，也帮个忙：</strong></p>
<ul>
<li><strong>在你的日常工作中，是否感受过 HTTPS/TLS 带来的性能或资源开销？你是如何应对的？</strong></li>
<li><strong>对于未来网络安全技术的发展，你认为在“更安全”与“更高效/更绿色”之间，我们应该如何权衡？</strong></li>
<li><strong>除了电力消耗，你认为 HTTPS/TLS 还带来了哪些“隐性”成本或效益？</strong></li>
</ul>
<p>欢迎在<strong>评论区</strong>留下你的思考和问题。如果你觉得这篇文章提供了一个有趣的视角，也请<strong>转发给你身边的朋友和同事</strong>，一起参与这个“思想实验”！</p>
<p><strong>想与我进行更深入的 Go 语言、网络安全与 AI 技术交流吗？</strong> 欢迎加入我的<strong>“Go &amp; AI 精进营”知识星球</strong>。</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<p>我们星球见！</p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/16/energy-savings-if-abandon-https/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>从简单到强大：再次探索Caddy服务器的魅力</title>
		<link>https://tonybai.com/2024/11/07/exploring-caddy/</link>
		<comments>https://tonybai.com/2024/11/07/exploring-caddy/#comments</comments>
		<pubDate>Wed, 06 Nov 2024 22:46:44 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ACME]]></category>
		<category><![CDATA[autosaved.json]]></category>
		<category><![CDATA[CA]]></category>
		<category><![CDATA[caddy]]></category>
		<category><![CDATA[caddy-l4]]></category>
		<category><![CDATA[Caddyfile]]></category>
		<category><![CDATA[Certificates]]></category>
		<category><![CDATA[certification]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[Handle]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[letsencrypt]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[match]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[RESTAPI]]></category>
		<category><![CDATA[reverseproxy]]></category>
		<category><![CDATA[rfc]]></category>
		<category><![CDATA[routes]]></category>
		<category><![CDATA[RPC]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[TLS-ALPN]]></category>
		<category><![CDATA[traefik]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[xcaddy]]></category>
		<category><![CDATA[ZeroSSL]]></category>
		<category><![CDATA[反向代理]]></category>
		<category><![CDATA[插件]]></category>
		<category><![CDATA[日志]]></category>
		<category><![CDATA[证书]]></category>
		<category><![CDATA[负载均衡]]></category>
		<category><![CDATA[路由]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4378</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/11/07/exploring-caddy Go语言诞生十多年来，社区涌现出众多优秀的Web服务器和反向代理解决方案。其中，最引人注目的无疑是Caddy和Traefik。这两者都为开发者和系统管理员提供了更简单、更安全的现代化Web服务器和反向代理部署选项。尽管它们的目标略有不同，Caddy最初旨在满足开发者快速搭建反向代理的需求，特别关注配置的简易性，并在后期增加了自动HTTPS和全面的API支持；而Traefik则更强调云原生架构，适合基于微服务的应用，尤其是使用Docker或Kubernetes部署的场景，提供动态服务发现和灵活的路由能力。 我于2015年首次体验了开源发布的Caddy，其超简单的配置确实给我留下了深刻的印象。之后也一直关注着Caddy的发展，Caddy在支持通过ACME协议自动为服务的域名获取免费HTTPS证书的功能后，Caddy就被我部署在自己的VPS上，为Gopher Daily等站点提供反向代理服务，运行十分稳定。Caddy这一为域名自动获取免费HTTPS证书的功能是其简化站点部署初衷的延续，也为Caddy赢得的广泛的用户和赞誉，并且这一特性不仅使得Caddy在个人项目和小型部署中大受欢迎，也让它在企业级应用中占有一席之地。 近10年后，我打算在这篇文章中再次探索一下Caddy，了解一下如今的Caddy都提供哪些强大的功能特性，为后续更好地使用Caddy做铺垫。 注：Caddy发展了近10年，支持了很多标准特性以及非标准特性(由社区提供，caddy官方不提供保证和support)，这里仅就笔者感兴趣的特性做探索。目前Caddy依靠sponsor的赞助进行着可持续演进，其所有标准功能都是免费的，但其作者Matt Holt也会为企业级赞助商进行定制功能开发。 1. Caddy的运行方法与基本配置 1.1 Caddy的启停 Caddy使用Go开发，因此继承了Go应用部署的一贯特点：只有一个可执行文件。将下载的Caddy放到\$PATH路径下，我们就可以在任意目录下执行它了： $caddy version v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= $caddy run 2024/10/11 07:56:24.664 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]} 这么启动后，caddy就会作为一个前台进程一直运行着，直到你停掉它。当然，我们也可以使用start命令将caddy作为后台进程启动： $caddy start 2024/10/11 08:32:07.557 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]} 2024/10/11 08:32:07.557 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/11/07/exploring-caddy">本文永久链接</a> &#8211; https://tonybai.com/2024/11/07/exploring-caddy</p>
<p><a href="https://tonybai.com/2023/11/11/go-opensource-14-years/">Go语言诞生十多年来</a>，社区涌现出众多优秀的Web服务器和反向代理解决方案。其中，最引人注目的无疑是<a href="https://caddyserver.com/">Caddy</a>和<a href="https://github.com/traefik/traefik">Traefik</a>。这两者都为开发者和系统管理员提供了更简单、更安全的现代化Web服务器和反向代理部署选项。尽管它们的目标略有不同，Caddy最初旨在满足开发者快速搭建反向代理的需求，特别关注配置的简易性，并在后期增加了自动HTTPS和全面的API支持；而Traefik则更强调云原生架构，适合基于微服务的应用，尤其是使用Docker或Kubernetes部署的场景，提供动态服务发现和灵活的路由能力。</p>
<p>我于2015年<a href="https://tonybai.com/2015/06/04/caddy-a-web-server-in-go/">首次体验了开源发布的Caddy</a>，其超简单的配置确实给我留下了深刻的印象。之后也一直关注着Caddy的发展，Caddy在支持通过<a href="https://datatracker.ietf.org/doc/html/rfc8555">ACME协议</a>自动为服务的域名获取免费HTTPS证书的功能后，Caddy就被我<a href="https://m.do.co/c/bff6eed92687">部署在自己的VPS上</a>，为<a href="https://gopherdaily.tonybai.com">Gopher Daily</a>等站点提供反向代理服务，运行十分稳定。Caddy这一为域名自动获取免费HTTPS证书的功能是其简化站点部署初衷的延续，也为Caddy赢得的广泛的用户和赞誉，并且这一特性不仅使得Caddy在个人项目和小型部署中大受欢迎，也让它在企业级应用中占有一席之地。</p>
<p>近10年后，我打算在这篇文章中再次探索一下Caddy，了解一下如今的Caddy都提供哪些强大的功能特性，为后续更好地使用Caddy做铺垫。</p>
<blockquote>
<p>注：Caddy发展了近10年，支持了很多标准特性以及非标准特性(由社区提供，caddy官方不提供保证和support)，这里仅就笔者感兴趣的特性做探索。目前Caddy依靠<a href="https://caddyserver.com/sponsor">sponsor的赞助</a>进行着可持续演进，其所有标准功能都是免费的，但其作者<a href="https://github.com/mholt">Matt Holt</a>也会为企业级赞助商进行定制功能开发。</p>
</blockquote>
<h2>1. Caddy的运行方法与基本配置</h2>
<h3>1.1 Caddy的启停</h3>
<p>Caddy使用Go开发，因此继承了Go应用部署的一贯特点：<strong>只有一个可执行文件</strong>。将下载的Caddy放到\$PATH路径下，我们就可以在任意目录下执行它了：</p>
<pre><code>$caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

$caddy run
2024/10/11 07:56:24.664 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]}
</code></pre>
<p>这么启动后，caddy就会作为一个前台进程一直运行着，直到你停掉它。当然，我们也可以使用start命令将caddy作为后台进程启动：</p>
<pre><code>$caddy start
2024/10/11 08:32:07.557 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]}
2024/10/11 08:32:07.557 INFO    serving initial configuration
Successfully started Caddy (pid=31215) - Caddy is running in the background
</code></pre>
<p>使用stop命令可以停到该后台进程：</p>
<pre><code>$caddy stop
2024/10/11 08:32:37.043 INFO    admin.api   received request    {"method": "POST", "host": "localhost:2019", "uri": "/stop", "remote_ip": "127.0.0.1", "remote_port": "65178", "headers": {"Accept-Encoding":["gzip"],"Content-Length":["0"],"Origin":["http://localhost:2019"],"User-Agent":["Go-http-client/1.1"]}}
2024/10/11 08:32:37.043 WARN    admin.api   exiting; byeee!!
2024/10/11 08:32:37.043 INFO    admin   stopped previous server {"address": "localhost:2019"}
2024/10/11 08:32:37.043 INFO    admin.api   shutdown complete   {"exit_code": 0}
</code></pre>
<h3>1.2 使用Caddyfile配置站点信息</h3>
<p>不过如此启动后的caddy并没有什么卵用，因为没有任何关于站点的配置信息。但caddy提供了config API（默认使用2019端口），我们可以使用下面方式访问该API：</p>
<pre><code>$curl localhost:2019/config/
null
</code></pre>
<p>由于没有任何配置数据，该接口返回null。Caddy提供了强大的API可以在Caddy运行是动态设置站点配置信息，这个我们后续再说，因为首次使用Caddy时，开发者通常更愿意使用Caddyfile来提供初始配置信息，Caddyfile也是最初caddy开源时唯一支持的配置方式。我们以server1.com为例来看看在本地使用caddy为其建立反向代理有多简单。下面是Caddyfile的内容：</p>
<pre><code>server1.com {
    tls internal
    reverse_proxy localhost:9001
}
</code></pre>
<p>然后我们基于该Caddyfile启动caddy，如果不显式传入配置文件，caddy默认使用当前目录(cwd)下的Caddyfile作为配置文件：</p>
<pre><code>$caddy run
2024/10/11 08:49:36.916 INFO    using adjacent Caddyfile
2024/10/11 08:49:36.920 INFO    adapted config to JSON  {"adapter": "caddyfile"}
2024/10/11 08:49:36.926 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2024/10/11 08:49:36.928 INFO    tls.cache.maintenance   started background certificate maintenance  {"cache": "0xc0005add80"}
2024/10/11 08:49:36.936 INFO    http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2024/10/11 08:49:36.936 INFO    http.auto_https enabling automatic HTTP-&gt;HTTPS redirects    {"server_name": "srv0"}
2024/10/11 08:49:36.964 WARN    pki.ca.local    installing root certificate (you might be prompted for password)    {"path": "storage:pki/authorities/local/root.crt"}
2024/10/11 08:49:37.024 INFO    warning: "certutil" is not available, install "certutil" with "brew install nss" and try again
2024/10/11 08:49:37.024 INFO    define JAVA_HOME environment variable to use the Java trust
Password:
2024/10/11 08:49:41.629 INFO    certificate installed properly in macOS keychain
2024/10/11 08:49:41.629 INFO    http    enabling HTTP/3 listener    {"addr": ":443"}
2024/10/11 08:49:41.632 INFO    http.log    server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2024/10/11 08:49:41.632 INFO    http.log    server running  {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2024/10/11 08:49:41.632 INFO    http    enabling automatic TLS certificate management   {"domains": ["server1.com"]}
2024/10/11 08:49:41.656 INFO    tls cleaning storage unit   {"storage": "FileStorage:/Users/tonybai/Library/Application Support/Caddy"}
2024/10/11 08:49:41.656 INFO    autosaved config (load with --resume flag)  {"file": "/Users/tonybai/Library/Application Support/Caddy/autosave.json"}
2024/10/11 08:49:41.656 INFO    serving initial configuration
2024/10/11 08:49:41.657 INFO    tls finished cleaning storage units
2024/10/11 08:49:41.657 INFO    tls.obtain  acquiring lock  {"identifier": "server1.com"}
2024/10/11 08:49:41.676 INFO    tls.obtain  lock acquired   {"identifier": "server1.com"}
2024/10/11 08:49:41.676 INFO    tls.obtain  obtaining certificate   {"identifier": "server1.com"}
2024/10/11 08:49:41.684 INFO    tls.obtain  certificate obtained successfully   {"identifier": "server1.com", "issuer": "local"}
2024/10/11 08:49:41.685 INFO    tls.obtain  releasing lock  {"identifier": "server1.com"}
2024/10/11 08:49:41.686 WARN    tls stapling OCSP   {"error": "no OCSP stapling for [server1.com]: no OCSP server specified in certificate", "identifiers": ["server1.com"]}
</code></pre>
<p>这段日志“信息量”很大，我们后面一点点来看。现在我们先验证一下caddy启动后是否能成功访问到server1.com这个“站点”，拓扑图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-2.png" alt="" /></p>
<p>server1.com的程序如下：</p>
<pre><code>// server1.go
package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello, server1.com")
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("Server is listening on port 9001...")
    if err := http.ListenAndServe("localhost:9001", nil); err != nil {
        fmt.Println("Error starting server:", err)
    }
}
</code></pre>
<p>启动server1后，我们使用curl访问server1.com（注：请先将server1.com放入/etc/hosts中，映射到本地127.0.0.1）：</p>
<pre><code>$go run server1.go
$curl https://server1.com
hello, server1.com
</code></pre>
<p>是不是非常简单 &#8211; <strong>短短几行配置就能在本地搭建出一个可以测试https站点的环境</strong>！</p>
<h3>1.3 Caddyfile背后的那些事儿</h3>
<p>现在是时候基于上面caddy run之后输出的日志以及Caddyfile的内容来说说caddy的一些运行机制了。</p>
<p>首先，当前版本的Caddy的<strong>默认配置信息格式</strong>已经不再是我们在Caddyfile中看到的那样了，而是改为了json格式。虽然上面我们是基于Caddyfile启动的caddy，但实际上caddy程序会在内部启用caddyfile adapt，将Caddyfile的格式转换为json格式后，再作为配置信息提供给caddy的后续逻辑：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-3.png" alt="" /></p>
<p>比如上面的Caddyfile被转换为json后的配置如下：</p>
<pre><code>{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "upstreams": [
                            {
                              "dial": "localhost:9001"
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "server1.com"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    },
    "tls": {
      "automation": {
        "policies": [
          {
            "issuers": [
              {
                "module": "internal"
              }
            ],
            "subjects": [
              "server1.com"
            ]
          }
        ]
      }
    }
  }
}
</code></pre>
<p>当然caddy也支持直接将该json格式配置作为启动时所需的初始配置文件：</p>
<pre><code>$caddy run --config caddy.json
</code></pre>
<p>即便是基于Caddyfile启动，caddy也会将当前配置自动保存起来(以下是macOS下启动caddy的日志)：</p>
<pre><code>2024/10/11 08:49:41.656 INFO    autosaved config (load with --resume flag)  {"file": "/Users/tonybai/Library/Application Support/Caddy/autosave.json"}
</code></pre>
<blockquote>
<p>注：linux上caddy默认保存config的位置为/var/lib/caddy/.config/caddy/autosave.json。</p>
</blockquote>
<p>正如日志中所提到的，下次启动时如果带上了&#8211;resume标志位，Caddy会基于自动保存的json配置文件启动！</p>
<p>如果caddy启动时带有&#8211;resume标志位，但在指定路径下找不到autosave.json时，它就会基于当前目录下的Caddyfile启动，除非使用&#8211;config指定配置文件。</p>
<p>在Caddyfile的server1.com site block中，我们使用<a href="https://caddyserver.com/docs/caddyfile/directives/tls">tls directive</a>：</p>
<pre><code>server1.com {
    tls internal
    reverse_proxy localhost:9001
}
</code></pre>
<p>tls directive的值是internal，意味着使用Caddy的内部、本地受信任的CA为本站点生成证书。Caddy会在本地创建自签的CA(默认名字是local)，并会尝试将自建的CA根证书安装到系统信任存储区，当以非特权用户运行Caddy时，可能会让你输入sudo用户的密码。接下来，Caddy就会用该CA为像server1.com这样的域名签发证书了。在macOS的用户的Library/Application Support/Caddy下我们能看到CA相关和为站点域名生成的相关私钥和证书：</p>
<pre><code>➜  /Users/tonybai/Library/Application Support/Caddy git:(master) ✗ $tree
.
├── autosave.json
├── certificates
│   └── local
│       └── server1.com
│           ├── server1.com.crt
│           ├── server1.com.json
│           └── server1.com.key
├── instance.uuid
├── last_clean.json
├── locks
└── pki
    └── authorities
        └── local
            ├── intermediate.crt
            ├── intermediate.key
            ├── root.crt
            └── root.key
</code></pre>
<h3>1.4 四层代理配置和grpc</h3>
<p>日常工作中，除了http/https代理，还有两个最常见的反向代理和负载均衡配置，一个是纯四层的Raw TCP和UDP，另外一个则是RPC(以gRPC最为广泛)。那么Caddy对这两种情况支持的如何呢？我们接下来就来看看。</p>
<h4>1.4.1 Raw TCP和UDP</h4>
<p>Caddy正式版目前不支持四层反向代理和负载均衡，但通过一些插件可以支持，其中<a href="https://github.com/mholt/caddy-l4/">mholt/caddy-l4</a>是其中最著名的，这也是由Caddy作者建立的项目，但目前还处于WIP状态，可以体验，但<strong>不建议用于生产环境</strong>。</p>
<p>由于Caddy是Go实现的，<a href="https://tonybai.com/2021/07/19/understand-go-plugin">Go对插件实现的方案方面不是很友好</a>，Caddy采用了重新编译的方案，但提供了名为<a href="https://github.com/caddyserver/xcaddy">xcaddy的构建工具</a>可以十分方便的支持带有插件的caddy编译，这也算将Go在编译方面的优势充分利用了起来了。</p>
<p>如果本地已经安装了go，那么安装xcaddy十分方便：</p>
<pre><code>$go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
go: downloading github.com/caddyserver/xcaddy v0.4.2
go: downloading github.com/Masterminds/semver/v3 v3.2.1
go: downloading github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
go: downloading github.com/josephspurrier/goversioninfo v1.4.0
go: downloading github.com/akavel/rsrc v0.10.2
</code></pre>
<p>接下来，我们就以用xcaddy编译带有mholt/caddy-l4插件了，这个过程大约持续1-2分钟吧，主要是下载依赖包耗时较长：</p>
<pre><code>$xcaddy build --with github.com/mholt/caddy-l4
2024/10/11 12:31:46 [INFO] absolute output file path: /Users/tonybai/caddy
2024/10/11 12:31:46 [INFO] Temporary folder: /Users/tonybai/buildenv_2024-10-17-1231.4160508500
2024/10/11 12:31:46 [INFO] Writing main module: /Users/tonybai/buildenv_2024-10-17-1231.4160508500/main.go
package main

import (
    caddycmd "github.com/caddyserver/caddy/v2/cmd"

    // plug in Caddy modules here
    _ "github.com/caddyserver/caddy/v2/modules/standard"
    _ "github.com/mholt/caddy-l4"
)

func main() {
    caddycmd.Main()
}
2024/10/11 12:31:46 [INFO] Initializing Go module
2024/10/11 12:31:46 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go mod init caddy
go: creating new go.mod: module caddy
go: to add module requirements and sums:
    go mod tidy
2024/10/11 12:31:46 [INFO] Pinning versions
2024/10/11 12:31:46 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v github.com/caddyserver/caddy/v2
go: -d flag is deprecated. -d=true is a no-op
go: downloading github.com/caddyserver/caddy v1.0.5
go: downloading github.com/caddyserver/caddy/v2 v2.8.4
go: downloading github.com/caddyserver/certmagic v0.21.3
go: downloading github.com/prometheus/client_golang v1.19.1
go: downloading github.com/quic-go/quic-go v0.44.0
go: downloading github.com/cespare/xxhash v1.1.0
go: downloading go.uber.org/zap/exp v0.2.0
go: downloading golang.org/x/term v0.20.0
go: downloading golang.org/x/time v0.5.0
go: downloading go.uber.org/multierr v1.11.0
... ...
go: added golang.org/x/term v0.20.0
go: added golang.org/x/text v0.15.0
go: added golang.org/x/time v0.5.0
go: added golang.org/x/tools v0.21.0
go: added google.golang.org/protobuf v1.34.1
2024/10/11 12:31:53 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v github.com/mholt/caddy-l4 github.com/caddyserver/caddy/v2
go: -d flag is deprecated. -d=true is a no-op
go: downloading github.com/mholt/caddy-l4 v0.0.0-20241012124037-5764d700c21c
go: accepting indirect upgrade from github.com/google/pprof@v0.0.0-20231212022811-ec68065c825e to v0.0.0-20240207164012-fb44976bdcd5
go: accepting indirect upgrade from github.com/miekg/dns@v1.1.59 to v1.1.62
go: accepting indirect upgrade from github.com/onsi/ginkgo/v2@v2.13.2 to v2.15.0
go: accepting indirect upgrade from golang.org/x/crypto@v0.23.0 to v0.28.0
go: accepting indirect upgrade from golang.org/x/mod@v0.17.0 to v0.18.0
go: accepting indirect upgrade from golang.org/x/net@v0.25.0 to v0.30.0
... ...
go: upgraded golang.org/x/sys v0.20.0 =&gt; v0.26.0
go: upgraded golang.org/x/term v0.20.0 =&gt; v0.25.0
go: upgraded golang.org/x/text v0.15.0 =&gt; v0.19.0
go: upgraded golang.org/x/time v0.5.0 =&gt; v0.7.0
go: upgraded golang.org/x/tools v0.21.0 =&gt; v0.22.0
2024/10/11 12:32:10 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v
go: -d flag is deprecated. -d=true is a no-op
go: downloading github.com/go-chi/chi/v5 v5.0.12
go: downloading gopkg.in/natefinch/lumberjack.v2 v2.2.1
go: downloading github.com/fxamacker/cbor/v2 v2.6.0
go: downloading github.com/google/go-tpm v0.9.0
... ...
go: downloading github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745
go: downloading github.com/go-logr/stdr v1.2.2
go: downloading github.com/cenkalti/backoff/v4 v4.2.1
go: downloading github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0
2024/10/11 12:32:15 [INFO] Build environment ready
2024/10/11 12:32:15 [INFO] Building Caddy
2024/10/11 12:32:15 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go mod tidy -e
go: downloading github.com/onsi/gomega v1.30.0
... ...
go: downloading golang.org/x/oauth2 v0.20.0
go: downloading cloud.google.com/go/auth/oauth2adapt v0.2.2
go: downloading github.com/google/s2a-go v0.1.7
go: downloading cloud.google.com/go/compute/metadata v0.3.0
go: downloading cloud.google.com/go/compute v1.24.0
go: downloading go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0
go: downloading github.com/googleapis/enterprise-certificate-proxy v0.3.2
2024/10/11 12:32:31 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go build -o /Users/tonybai/caddy -ldflags -w -s -trimpath -tags nobadger
2024/10/11 12:33:22 [INFO] Build complete: ./caddy
2024/10/11 12:33:22 [INFO] Cleaning up temporary folder: /Users/tonybai/buildenv_2024-10-17-1231.4160508500

././caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
</code></pre>
<p>编译后得到的caddy放在当前目录下：</p>
<pre><code>$./caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
</code></pre>
<p>为了与原先的caddy做区分，我们将新编译出来的caddy重命名为caddy-with-l4。下面我们就来看一个四层负载均衡的示例，先看一下Caddyfile的配置：</p>
<pre><code>{
    layer4 {
        127.0.0.1:5000 {
            route {
                proxy localhost:9003 localhost:9004 {
                    lb_policy round_robin
                }
            }
        }
    }
}
</code></pre>
<p>这个配置非常好理解！如下面示意图，caddy将来自客户端到5000端口的连接按照round robin负载均衡算法分配到后面的两个服务localhost:9003和localhost:9004上：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-4.png" alt="" /></p>
<p>看完TCP，我们再来看看UDP的反向代理的例子，我们修改一下Caddyfile：</p>
<pre><code>{
    layer4 {
        udp/127.0.0.1:5000 {
            route {
                proxy udp/localhost:9005 udp/localhost:9006 {
                    lb_policy round_robin
                }
            }
        }
    }
}
</code></pre>
<p>这个配置同样非常好理解！如下面示意图，caddy将来自客户端到5000端口的udp连接按照round robin负载均衡算法分配到后面的两个服务localhost:9005和localhost:9006上：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-5.png" alt="" /></p>
<blockquote>
<p>注：关于上面两个tcp和udp的示例的client端和server端的代码，可以在github.com/bigwhite/experiments下的caddy-examples中找到，这里鉴于篇幅，就不贴出来了。</p>
</blockquote>
<p>接下来，我们再看看RPC。</p>
<h4>1.4.2 RPC</h4>
<p>我们以最为流行的<a href="https://tonybai.com/2021/09/17/those-things-about-grpc-client">gRPC</a>为例，来看看如何配置Caddy，试验拓扑如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-6.png" alt="" /></p>
<p>请提前将rpc-server.com配置到/etc/hosts中，ip为localhost。然后，根据上面拓扑图，我们将Caddyfile更新为下面内容：</p>
<pre><code>rpc-server.com {
    tls internal
    reverse_proxy h2c://localhost:9007 h2c://localhost:9008
}
</code></pre>
<p>gRPC使用HTTP/2帧，h2c://可以确保后端启用明文HTTP/2。</p>
<blockquote>
<p>注：关于gRPC的grpc-client、grpc-server1和grpc-server2的代码，可以在github.com/bigwhite/experiments下的caddy-examples的rpc目录中找到，这里鉴于篇幅，就不贴出来了。</p>
</blockquote>
<p>到这里，关于Caddy的运行方法以及针对各种协议的基本配置方法已经初步探索完了，接下来我们再来看一下Caddy的另一个强大的功能：基于API的运行时动态配置。</p>
<h2>2. 运行时使用API对Caddy进行动态配置</h2>
<p><a href="https://caddyserver.com/docs/api">Caddy提供了admin和config API</a>，允许我们在运行时动态配置和管理服务器。前面提到过，Caddy默认的API端口和路径是http://localhost:2019/config/。不过，需要注意的是：<strong>通过API设置的路由配置仅存储在内存中，并未持久化</strong>。这意味着当Caddy服务器重启后，如果没有使用&#8211;resume恢复autosave.json中的配置，那么之前通过API进行的各种设置将失效。</p>
<p>在Caddy提供的API中，我们最关心的还是与服务器(server)、路由(routes)、处理器(handle)以及匹配器(match)的设置，以下面Caddyfile所表示的https服务器设置为例：</p>
<pre><code>server1.com {
    tls internal
    reverse_proxy localhost:9001
}
server2.com {
    tls internal
    reverse_proxy localhost:9002 localhost:9012
}
</code></pre>
<p>该Caddyfile对应的拓扑图如下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-8.png" alt="" /></p>
<p>该Caddyfile转换为JSON格式后的配置数据如下：</p>
<pre><code>{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "upstreams": [
                            {
                              "dial": "localhost:9001"
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "server1.com"
                  ]
                }
              ],
              "terminal": true
            },
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "upstreams": [
                            {
                              "dial": "localhost:9002"
                            },
                            {
                              "dial": "localhost:9012"
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "server2.com"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    },
    "tls": {
      "automation": {
        "policies": [
          {
            "issuers": [
              {
                "module": "internal"
              }
            ],
            "subjects": [
              "server1.com",
              "server2.com"
            ]
          }
        ]
      }
    }
  }
}
</code></pre>
<p>其中，我们关注的服务器(server)、路由(routes)、处理器(handle)和匹配器(match)之间的隶属关系如下图，其他配置将由Caddy自动完成：</p>
<p><img src="https://tonybai.com/wp-content/uploads/exploring-caddy-7.png" alt="" /></p>
<p>接下来，我们就基于这个示例，来看看通过Caddy API如何完成一些常见的站点设置操作。</p>
<h3>2.1 POST /load</h3>
<p>我们先看看整体替换的POST /load接口。通过该接口，我们可以用新的Caddy配置整体覆盖当前生效的Caddy配置，Caddy收到这个请求后，会阻塞住该调用，直到新配置加载完成或加载失败才会返回。如果加载失败，Caddy会回滚之前的配置。与caddy reload命令一样，该接口可以实现不停机更新并生效配置，无论是加载成功还是加载失败回滚。</p>
<p>下面我们修改一下上面json，将server2.com路由中的那个监听9012的upstream server去掉，并保存为caddy-load.json。如果担心自己修改的配置信息不正确，可以在调用接口之前，先用caddy validate对caddy-load.json进行有效性检查：</p>
<pre><code>$caddy validate -c caddy-load.json
2024/10/11 02:50:28.649 INFO    using config from file  {"file": "caddy-load.json"}
2024/10/11 02:50:28.651 INFO    tls.cache.maintenance   started background certificate maintenance  {"cache": "0xc00012dd00"}
2024/10/11 02:50:28.652 INFO    http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2024/10/11 02:50:28.652 INFO    http.auto_https enabling automatic HTTP-&gt;HTTPS redirects    {"server_name": "srv0"}
2024/10/11 02:50:28.652 INFO    tls.cache.maintenance   stopped background certificate maintenance  {"cache": "0xc00012dd00"}
Valid configuration
</code></pre>
<p>然后用下面curl命令调用load接口尝试新配置加载：</p>
<pre><code>$curl "http://localhost:2019/load" \
    -H "Content-Type: application/json" \
    -d @caddy-load.json
</code></pre>
<p>此时Caddy会输出类似如下日志：</p>
<pre><code>2024/10/11 02:53:15.191 INFO    admin.api   received request    {"method": "POST", "host": "localhost:2019", "uri": "/load", "remote_ip": "127.0.0.1", "remote_port": "60898", "headers": {"Accept":["*/*"],"Content-Length":["1968"],"Content-Type":["application/json"],"Expect":["100-continue"],"User-Agent":["curl/7.54.0"]}}
2024/10/11 02:53:15.226 INFO    admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//[::1]:2019", "//127.0.0.1:2019", "//localhost:2019"]}
2024/10/11 02:53:15.240 INFO    http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2024/10/11 02:53:15.240 INFO    http.auto_https enabling automatic HTTP-&gt;HTTPS redirects    {"server_name": "srv0"}
2024/10/11 02:53:15.254 INFO    pki.ca.local    root certificate is already trusted by system   {"path": "storage:pki/authorities/local/root.crt"}
2024/10/11 02:53:15.256 INFO    http    enabling HTTP/3 listener    {"addr": ":443"}
2024/10/11 02:53:15.257 INFO    http.log    server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2024/10/11 02:53:15.257 INFO    http.log    server running  {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2024/10/11 02:53:15.257 INFO    http    enabling automatic TLS certificate management   {"domains": ["server1.com", "server2.com"]}
2024/10/11 02:53:15.257 INFO    http    servers shutting down with eternal grace period
2024/10/11 02:53:15.258 INFO    autosaved config (load with --resume flag)  {"file": "/Users/tonybai/Library/Application Support/Caddy/autosave.json"}
2024/10/11 02:53:15.258 INFO    admin.api   load complete
2024/10/11 02:53:15.263 INFO    admin   stopped previous server {"address": "localhost:2019"}
</code></pre>
<p>更新后，你可以通过config API或autosaved.json查看变更后的配置，也可以通过测试验证新配置是否生效。</p>
<p>不过，这种整体替换显然更容易失败，如果Caddy代理的站点路由很多，json文件的Size也不可小觑。此外，要维护全量的配置，还要对Caddy的配置有较为系统的了解。在日常维护中，按配置路径更新局部配置更为实用一些，接下来我们就来看看如何基于配置路径管理服务器(server)、路由(routes)、处理器(handle)以及匹配器(match)的设置。</p>
<h3>2.2 /config/&#91;path&#93;</h3>
<p>通过在config后面加上要操作的配置路径，我们可以读取和更新对应路径上的配置信息。</p>
<h4>2.2.1 读取特定路径下的配置</h4>
<p>使用Http Get请求，可以读取在/config后面的指定路径上的配置。</p>
<ul>
<li>读取全部</li>
</ul>
<pre><code>$curl "http://localhost:2019/config/"
</code></pre>
<ul>
<li>读取所有服务器(server)配置</li>
</ul>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers"
{"srv0":{"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9001"}]}]}]}],"match":[{"host":["server1.com"]}],"terminal":true},{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9002"},{"dial":"localhost:9012"}]}]}]}],"match":[{"host":["server2.com"]}],"terminal":true}]}}
</code></pre>
<ul>
<li>读取某个服务器(server)的配置</li>
</ul>
<p>以srv0为例：</p>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0"
{"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9001"}]}]}]}],"match":[{"host":["server1.com"]}],"terminal":true},{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9002"},{"dial":"localhost:9012"}]}]}]}],"match":[{"host":["server2.com"]}],"terminal":true}]}
</code></pre>
<ul>
<li>读取srv0的listen配置</li>
</ul>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0/listen/"
[":443"]
</code></pre>
<ul>
<li>读取srv0的所有路由</li>
</ul>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0/routes/"
[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9001"}]}]}]}],"match":[{"host":["server1.com"]}],"terminal":true},{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9002"},{"dial":"localhost:9012"}]}]}]}],"match":[{"host":["server2.com"]}],"terminal":true}]
</code></pre>
<p>路由是一个数组，要读取某个路由，可以使用数组下标，比如：</p>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0/routes/0/"
{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9001"}]}]}]}],"match":[{"host":["server1.com"]}],"terminal":true}
</code></pre>
<ul>
<li>读取某路由的handle和match</li>
</ul>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0/routes/0/handle/"
[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:9001"}]}]}]}]

$curl "http://localhost:2019/config/apps/http/servers/srv0/routes/0/match/"
[{"host":["server1.com"]}]
</code></pre>
<p>我们看到，就像上面这样按配置路径逐步细化，便可以读取到所有对应的配置，遇到数组类型，可以使用下标读取对应的“数组元素”的配置。</p>
<p>接下来，我们再来看看基于路径的配置修改方法。</p>
<h4>2.2.2 更新特定路径下的配置</h4>
<p>使用Http Post请求，可以创建或更新在/config后面的指定路径上的配置。如果指定路径对应的配置目标为一个数组，则POST会将json作为元素追加到数组中；如果目标是一个对象，则post会基于json信息创建新对象或更新对象。</p>
<p>我们先以apps/http/servers/srv0/listen/这个数组对象为例，为其添加一个新元素”:80&#8243;：</p>
<pre><code>$curl -H "Content-Type: application/json" -d '":80"' "http://localhost:2019/config/apps/http/servers/srv0/listen"
</code></pre>
<p>成功之后，我们可以看到listen数组的变化：</p>
<pre><code>$curl "http://localhost:2019/config/apps/http/servers/srv0/listen"
[":443",":80"]
</code></pre>
<p>如果是要更改某个数组元素，我们可以使用PATCH请求，比如将刚刚创建的”:80&#8243;改为”:90&#8243;：</p>
<pre><code>$curl -X PATCH -H "Content-Type: application/json" -d '":90"' "http://localhost:2019/config/apps/http/servers/srv0/listen/1"
$curl "http://localhost:2019/config/apps/http/servers/srv0/listen"
[":443",":90"]
</code></pre>
<p>如果要删除刚才添加的数组元素，可以使用DELETE请求，根据下标值路径进行删除：</p>
<pre><code>$curl -X DELETE  "http://localhost:2019/config/apps/http/servers/srv0/listen/1"
$curl "http://localhost:2019/config/apps/http/servers/srv0/listen"
[":443"]
</code></pre>
<p>下面我们来添加一个srv1对象，与上面的srv0并齐：</p>
<pre><code>$curl -H "Content-Type: application/json" -d '{ "listen" : [":444"]}' "http://localhost:2019/config/apps/http/servers/srv1/"
</code></pre>
<p>创建后，我们得到下面配置：</p>
<pre><code>$curl  "http://localhost:2019/config/apps/http/servers/" | gojq
{
  "srv0": {
    "listen": [
      ":443"
    ],
    "routes": [
      ... ...
    ]
  },
  "srv1": {
    "listen": [
      ":444"
    ]
  }
}
</code></pre>
<p>但我们不能这么创建：</p>
<pre><code>$curl -H "Content-Type: application/json" -d '{ "srv1" : { "listen" : [":444"]}}' "http://localhost:2019/config/apps/http/servers/"
</code></pre>
<p>这样会覆盖掉servers的全部信息，整个servers信息将变为：</p>
<pre><code>$curl  "http://localhost:2019/config/apps/http/servers/" | gojq
{
  "srv1": {
    "listen": [
      ":444"
    ]
  }
}
</code></pre>
<h3>2.3 @id</h3>
<p>虽然通过上面指定路径可以获取和更新对应的配置，但我们也看到了Caddy的json的缩进非常深，这给API的调用者带来了心智负担。Caddy提供了一种强大而灵活的方式来快速访问和修改配置中的特定部分，这就是使用@id标识符。通过在配置中为某些元素分配唯一的@id，我们可以直接引用这些元素，而无需指定完整的路径。这在处理复杂配置或需要频繁修改特定部分时特别有用。</p>
<p>在Caddy的配置中，@id可以应用于多个层次的配置元素。具体来说，在apps/http/servers下的各个层次都支持@id，包括但不限于：</p>
<ul>
<li>服务器（server）级别</li>
<li>路由（routes）级别</li>
<li>处理器（handle）级别</li>
<li>匹配器（match）级别</li>
</ul>
<p>下面让我们通过具体的例子来看看如何在这些不同的层次上使用@id。由于Caddyfile不支持@id，我们将使用新的配置作为示例：</p>
<p>我们建立一个新的json作为Caddy的启动配置文件：</p>
<pre><code>{
  "apps": {
    "http": {
      "servers": {
        "myserver": {
          "@id": "main_server",
          "listen": [
            ":80"
          ],
          "routes": [
            {
              "@id": "main_route",
              "handle": [
                {
                  "@id": "main_handler",
                  "body": "Hello from main server!",
                  "handler": "static_response"
                }
              ],
              "match": [
                {
                  "@id": "path_matcher",
                  "path": [
                    "/api/*"
                  ]
                }
              ]
            }
          ]
        }
      }
    }
  }
}
</code></pre>
<p>我们先看看服务器级别的@id使用。在这里我们为myserver这个服务器赋予了一个新的@id字段，值为main_server，接下来，我们就可以使用下面路径获取和更新该server的配置信息：</p>
<pre><code>$curl  "http://localhost:2019/id/main_server"
{"@id":"main_server","listen":[":80"],"routes":[{"handle":[{"body":"Hello from main server!","handler":"static_response"}]}]}

$curl  "http://localhost:2019/id/main_server/listen"
[":80"]
</code></pre>
<p>同理，在路由级别，我们也为为其中的一个路由设置了@id字段，值为main_route，通过下面命令便可以获取和更新该路由信息：</p>
<pre><code>$curl  "http://localhost:2019/id/main_route/"
{"@id":"main_route","handle":[{"@id":"main_handler","body":"Hello from main server!","handler":"static_response"}],"match":[{"@id":"path_matcher","path":["/api/*"]}]}

$curl  "http://localhost:2019/id/main_route/handle"
[{"@id":"main_handler","body":"Hello from main server!","handler":"static_response"}]
</code></pre>
<p>通过handle（处理器）级别的@id，我们同样可以直接访问@id对应的对象的信息：</p>
<pre><code>$curl  "http://localhost:2019/id/main_handler/"
{"@id":"main_handler","body":"Hello from main server!","handler":"static_response"}

$curl  "http://localhost:2019/id/main_handler/body"
"Hello from main server!"
</code></pre>
<p>最后是通过@id访问matcher：</p>
<pre><code>$curl  "http://localhost:2019/id/path_matcher/"
{"@id":"path_matcher","path":["/api/*"]}

$curl  "http://localhost:2019/id/path_matcher/path"
["/api/*"]
</code></pre>
<p>我们看到：使用@id方式，我们可以像一个使用指针或传送点那样，直达特定路径下面，而无需一层一层的输入路径信息。在处理大型或复杂的配置时，它为管理员和开发者提供了一种更灵活、更直观的方式来操作Caddy的配置。</p>
<h2>3. 生产环境的实践与ACME</h2>
<p>最后我们来简单说说在生产环境使用Caddy的一些实践方法。</p>
<h3>3.1 生产环境的Caddy配置方法</h3>
<p>前面说了那么多的Caddy配置方法，那么在生产环境究竟应该使用哪种方法来进行Caddy的初始配置、运行时动态配置更新以及配置的持久化呢？</p>
<p>虽然Caddyfile简单，但如果要在生产环境中进行运行时的动态配置更新，json格式才是不二之选，我们首先可以基于标准格式准备一份json的初始配置作为caddy的初始启动配置，这个配置后续就可以不再使用了。</p>
<p>启动caddy时建议使用&#8211;resume，初始情况下因为还没有autosaved.json，caddy会基于初始配置启动，之后重启caddy都会基于autosaved.json启动。</p>
<p>而运行时，我们可直接<strong>基于API对caddy的配置进行修改</strong>，所有的修改都会立即生效，而且无需停机，并且配置变更会save到autosave.json中，即便caddy重启，下一次启动时caddy也会加载停机前的最新配置，而这一切都不需要我们干预。</p>
<h3>3.2 自动HTTPS与ACME</h3>
<p>在生产环境使用Caddy，除了其超级简单的配置和相对不错的性能之外，最主要就要用它的自动https，即自动为代理的站点域名从<a href="https://letsencrypt.org/">Let&#8217;s Encrypt</a>或<a href="https://zerossl.com/">zerossl</a>申请受信任的免费证书，并可以在证书过期前自动更新证书。Caddy是通过<a href="https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment">ACME协议</a>与这两个站点进行交互并获取和维护证书的。</p>
<p>ACME协议是一个用于自动化数字证书管理的协议。它允许服务器或客户端软件自动向证书颁发机构 (CA) 请求、更新和撤销SSL/TLS证书。ACME协议的优势在于减少了人为错误，支持短期证书，提高了证书安全性，同时由于支持自动化，让大规模证书部署和管理成为可能。</p>
<p>该协议最早在2015年由Let&#8217;s Encrypt推出，旨在推广HTTPS，并使证书管理自动化和标准化。</p>
<p>ACME的API版本有两个，API v1规范于2016年发布。它支持为完全限定的域名颁发证书，例如example.com或cluster.example.com，但不支持&#42;.example.com等通配符证书。API v2规范于2018年发布，被称为ACME v2，ACME v2不向后兼容v1。v2版本支持通配符域名证书，例如&#42;.example.com。同时新增新的挑战(challenge)类型TLS-ALPN-01。</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc8555">IETF在2019年正式将ACME作为标准协议发布(RFC 8555)</a>。2021年，ACME v1版本废弃，不再提供支持。</p>
<p>ACME协议的主要组件包括客户端、ACME服务器（如Let&#8217;s Encrypt或ZeroSSL）、挑战机制（Challenges）以及证书颁发流程。客户端首先向ACME服务器请求证书，<strong>服务器通过挑战机制要求客户端证明对域名的控制权</strong>，验证通过后颁发证书。这里最复杂的就是挑战机制了。</p>
<p>Caddy Server支持以下ACME 挑战机制：</p>
<ul>
<li>HTTP Challenge</li>
</ul>
<p>CA机构执行该挑战时会对候选主机名的A/AAAA记录执行权威DNS查找，然后在端口80上使用HTTP请求一个临时的加密资源。如果CA（证书颁发机构）看到了预期的资源，则会颁发证书。该挑战机制要求端口80必须对外部可访问。在Caddy中，此挑战机制默认启用且无需显式配置。</p>
<ul>
<li>TLS-ALPN Challenge</li>
</ul>
<p>CA机构执行该挑战时会对候选主机名的A/AAAA记录执行权威DNS查找，然后在端口443上使用一个包含特殊ServerName和ALPN值的TLS握手请求临时的加密资源。如果CA看到了预期的资源，则会颁发证书。该挑战机制要求端口443必须对外部可访问。在Caddy中，此挑战机制也是默认启用的，且无需显式配置。</p>
<ul>
<li>DNS Challenge</li>
</ul>
<p>CA机构执行该挑战时会对候选主机名的TXT记录执行权威DNS查找，并查找包含特定值的TXT记录。如果CA看到了预期的值，则会颁发证书。</p>
<p>该挑战机制的优点是无需开放任何端口，并且请求证书的服务器不需要对外部可访问。但需要Caddy配置访问候选主机域名的DNS提供商的凭据(api token)，以便Caddy能够通过api设置（和清除）特殊的TXT记录。如果启用了DNS挑战，默认情况下其他挑战会被禁用。</p>
<p>这三种挑战机制在不同场景下都有各自的优势，Caddy默认启用HTTP和TLS-ALPN挑战，并在需要时会自动选择最成功的挑战类型来使用。同时Caddy也为DNS challenge提供了对各种DNS提供商的插件支持，这些插件可以在<a href="https://github.com/caddy-dns">https://github.com/caddy-dns</a>中查找。</p>
<p>Go在ACME方面有着广泛的应用，很多标准的<a href="https://letsencrypt.org/docs/client-options/#clients-go">ACME client</a>以及服务端都是由go实现的，比如<a href="https://github.com/cert-manager/cert-manager">cert-manager</a>等，甚至包括支撑let&#8217;s encrypt自身的服务都是基于Go实现的，即<a href="https://github.com/letsencrypt/boulder">用于实现CA的boulder开源项目</a>。</p>
<h2>4. 小结</h2>
<p>在本文中，我们深入探索了Caddy服务器的强大功能与简便配置。Caddy以其独特的设计理念，简化了Web服务器和反向代理的搭建过程，尤其是在自动HTTPS证书管理和API支持方面表现突出。通过Caddyfile的简单配置，用户可以迅速部署安全的HTTPS站点，而无需繁琐的步骤。</p>
<p>此外，Caddy的动态配置能力使得在运行时调整服务器设置成为可能，极大提高了灵活性和管理效率。尽管Caddy目前在四层代理和负载均衡的支持上还有待增强，但通过插件的方式也为用户提供了扩展的可能性。</p>
<p>总之，Caddy不仅适合个人项目的快速搭建，也在企业级应用中展现出强大的稳定性和高效性。随着社区的不断发展和支持，Caddy将继续成为开发者和系统管理员的重要工具。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/caddy-examples">这里</a>下载。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/11/07/exploring-caddy/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解Web应用的机密管理</title>
		<link>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/</link>
		<comments>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/#comments</comments>
		<pubDate>Tue, 07 Nov 2023 21:58:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ACL]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Barrier]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[consul]]></category>
		<category><![CDATA[credentials]]></category>
		<category><![CDATA[CRUD]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[HA]]></category>
		<category><![CDATA[hashicorp]]></category>
		<category><![CDATA[HSM]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[jwt]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[KeePass]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[KeyWhiz]]></category>
		<category><![CDATA[KMS]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[nomad]]></category>
		<category><![CDATA[OTP]]></category>
		<category><![CDATA[rotate]]></category>
		<category><![CDATA[RSA]]></category>
		<category><![CDATA[SDK]]></category>
		<category><![CDATA[sealed]]></category>
		<category><![CDATA[secret]]></category>
		<category><![CDATA[secret-management]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[terraform]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[unseal]]></category>
		<category><![CDATA[vault]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[一次性密码]]></category>
		<category><![CDATA[令牌]]></category>
		<category><![CDATA[凭据]]></category>
		<category><![CDATA[凭证]]></category>
		<category><![CDATA[密码]]></category>
		<category><![CDATA[密钥]]></category>
		<category><![CDATA[授权]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[日志]]></category>
		<category><![CDATA[明文]]></category>
		<category><![CDATA[机密管理]]></category>
		<category><![CDATA[身份认证]]></category>
		<category><![CDATA[配置文件]]></category>
		<category><![CDATA[高可用]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4037</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example 如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着CRUD的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？ 你只是大多应用系统开发人员中的一个典型代表。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现： 明文存储密码、密钥等敏感数据 很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。 硬编码密钥与密码 重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。 使用弱密码、未定期更换 使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。 不同环境复用同一密钥 开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。 明文传输密码 HTTP传输中明文传递密码，导致可被嗅探截获。 日志中输出明文密码 调试日志中直接输出数据库密码等敏感信息，可能被利用。 缺乏访问控制和审计机制 密钥等资源无访问控制，且操作不被审计，难以追踪。 这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。 在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。 1. 认识机密管理 在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。 机密管理拥有一套自己的核心管理措施，包括： 从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储; 执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限； 执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计； 定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)； 自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略； &#8230; &#8230; 机密管理涉及要管理的机密信息的类型包括(但不限于)： 用户密码或自动生成的密码 API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证） SSH密钥 数据库和其他system-to-system的密码 用于安全通信、传输和接收数据的私人证书（TLS、SSL 等） RSA和其他一次性密码设备 综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的机密管理系统来应对。 2. 机密管理系统 机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段： 文件加密阶段 早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。 自建解决方案阶段 企业开始自研一些机密管理解决方案(包括基于一些像KeePass这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。 开源机密管理项目 随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：Vault、Keywhiz等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。 注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example</p>
<p>如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着<a href="https://www.codecademy.com/article/what-is-crud">CRUD</a>的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？</p>
<p>你只是大多应用系统开发人员中的一个<strong>典型代表</strong>。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现：</p>
<ul>
<li>明文存储密码、密钥等敏感数据</li>
</ul>
<p>很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。</p>
<ul>
<li>硬编码密钥与密码</li>
</ul>
<p>重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。</p>
<ul>
<li>使用弱密码、未定期更换</li>
</ul>
<p>使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。</p>
<ul>
<li>不同环境复用同一密钥</li>
</ul>
<p>开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。</p>
<ul>
<li>明文传输密码</li>
</ul>
<p>HTTP传输中明文传递密码，导致可被嗅探截获。</p>
<ul>
<li>日志中输出明文密码</li>
</ul>
<p>调试日志中直接输出数据库密码等敏感信息，可能被利用。</p>
<ul>
<li>缺乏访问控制和审计机制</li>
</ul>
<p>密钥等资源无访问控制，且操作不被审计，难以追踪。</p>
<p>这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。</p>
<p>在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。</p>
<h2>1. 认识机密管理</h2>
<p>在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。</p>
<p>机密管理拥有一套自己的核心管理措施，包括：</p>
<ul>
<li>从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储;</li>
<li>执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限；</li>
<li>执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计；</li>
<li>定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)；</li>
<li>自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略；</li>
<li>&#8230; &#8230;</li>
</ul>
<p>机密管理涉及要管理的机密信息的类型包括(但不限于)：</p>
<ul>
<li>用户密码或自动生成的密码</li>
<li>API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证）</li>
<li>SSH密钥</li>
<li>数据库和其他system-to-system的密码</li>
<li>用于安全通信、传输和接收数据的私人证书（TLS、SSL 等）</li>
<li>RSA和其他一次性密码设备</li>
</ul>
<p>综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的<strong>机密管理系统</strong>来应对。</p>
<h2>2. 机密管理系统</h2>
<p>机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段：</p>
<ul>
<li>文件加密阶段</li>
</ul>
<p>早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。</p>
<ul>
<li>自建解决方案阶段</li>
</ul>
<p>企业开始自研一些机密管理解决方案(包括基于一些像<a href="https://keepass.info">KeePass</a>这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。</p>
<ul>
<li>开源机密管理项目</li>
</ul>
<p>随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：<a href="https://github.com/hashicorp/vault/">Vault</a>、<a href="https://github.com/square/keywhiz">Keywhiz</a>等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。</p>
<blockquote>
<p>注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp Vault。</p>
</blockquote>
<ul>
<li>云原生机密管理平台</li>
</ul>
<p>在开源机密管理项目的基础之上，这些开源项目背后的开发商以及一些专业的公有云提供商开始面向云原生应用和DevOps，以SaaS形式提供专业的机密管理服务和全面的机密管理解决方案，如<a href="https://azure.microsoft.com/en-us/products/key-vault/">Azure Key Vault</a>、<a href="https://cloud.google.com/secret-manager">Google Secret Manager</a>、<a href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a>、HashiCorp Vault等。</p>
<p>我们看到：专业的机密系统发展到今天的水平，其过程不是一蹴而就的。正是基于历史经验的积累和总结，现代机密管理平台才演化出了面向云原生架构、支持DevOps、细粒度访问控制、机密信息的动态化以及生命周期的自动化管理等先进功能特性。</p>
<p>在上面的优秀的云原生机密管理系统中，HashiCorp Vault是唯一开源且可以私有化部署在企业内部的。<a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp公司于2015年发布并开源了Vault</a>，经过多年发展，Vault已经发展成为一款功能强大的企业级机密管理系统，并被广泛视为云原生领域的首选解决方案。</p>
<p>对于普通Web应用开发者而言，<strong>既要有机密管理的意识，又要有机密管理的实现手段</strong>。HashiCorp Vault的设计目标之一就是将机密管理下沉到平台层面，让应用开发者能够专注于应用程序的开发而无需过多关注机密的管理和保护。</p>
<p>作为Web应用开发者，基于Vault实现Web应用的机密管理是一条非常可行的机密管理方案。通过与Vault的集成，Web应用开发者可以利用Vault提供的丰富功能来处理各种机密管理需求和场景。开发者只需要学习如何使用Vault的API或客户端库与Vault进行交互，就能轻松地访问和管理机密数据，实现机密信息(如数据库凭据、API 密钥等)获取、动态机密信息生成、访问控制、审计和监控等机密管理功能，并且可以减少机密管理的开发和维护的复杂性。</p>
<p>接下来，我就和大家一起简要的了解一下Hashicorp的Vault。</p>
<h2>3. 认识Vault</h2>
<h3>3.1 Vault的架构</h3>
<p>如果对Hashicorp这家公司很熟悉，你肯定知道Hashicorp大部分产品(和开源项目)都是由Go开发的，包括<a href="https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/">consul</a>、<a href="https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/">nomad</a>、<a href="https://github.com/hashicorp/terraform">terraform</a>以及<a href="https://github.com/hashicorp/vagrant">vagrant</a>(<a href="https://discuss.hashicorp.com/t/status-of-vagrant-go/42562">vagrant的新版本将切换到go实现</a>)等。</p>
<p>Vault这款优秀的机密管理软件系统继承了Hashicorp的开发基因，也是由Go语言开发的。从2015年至今，Vault已经演化为一个功能强大，但相对也比较复杂的系统，下面是<a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Hashicorp官方架构文档</a>中的一个关于Vault的high level的结构示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-2.png" alt="" /></p>
<p>从整体架构设计思路来看，vault支持：</p>
<ul>
<li>高可用性</li>
</ul>
<p>Vault的架构设计允许部署多个Vault服务器以实现高可用性和容错性，在<a href="https://developer.hashicorp.com/vault/docs/internals/high-availability">高可用集群部署模式</a>下，多个vault服务器共享存储后端，并且每个vault服务器可能是两个状态：active和standby。任意时刻集群都只有一个实例处于active状态，所有standby实例都处于热备用状态(hot standby)。只有处于active状态的服务器会处理所有请求；standby服务器会将所有请求重定向到活动Vault服务器，这点与consul的设计是一致的。如果active服务器被sealed、发生故障或失去网络连接，则standby Vault服务器中的一个将成为active实例。</p>
<p>这里有人可能会问：如果只有一个active实例，那么在访问量增大的时候，active实例便会成为热点或性能瓶颈！没错，这是vault开源版本的约束。这个约束在vault的企业付费版中被取消，在付费版中，standby服务器可以接收只读请求，所有只读请求会均衡分担到各个standby实例上，如果standby实例收到写请求，它会将写请求转发给active实例处理。</p>
<ul>
<li>封存和解封</li>
</ul>
<p>说高可用性时，我们提到了vault服务器实例的sealed(封存)状态。启动Vault服务器时，它会处于sealed状态。在这种状态下，Vault仅知道访问物理存储的位置和方式，但不知道如何解密存储中数据。在unseal(解封)之前，该vault服务器几乎无法做任何操作。在对处于sealed状态的Vault实例进行任何操作之前，必须对其进行解封(unseal)。</p>
<p>解封操作需要提供<strong>解封密钥(unseal keys)</strong>。有人注意到了，我用了unseal keys，而不是unseal key，因为解封密钥是由一种名为<a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir&#8217;s Secret Sharing</a>的算法分解保存和汇集生成的。Shamir&#8217;s Secret Sharing（Shamir的机密分享算法）是一种密码学算法，用于将机密数据(在本文中指的就是“unseal key”)分割成多个部分，称为shares。这些share可以被分发给不同的人，如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-3.png" alt="" /></p>
<p>而只有当足够数量的share被汇集时，才能恢复出原始的机密数据(unseal key)，并用恢复出的机密数据进行下一步操作(如下图所示，下图来自Hashicorp官方文档)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-4.png" alt="" /></p>
<p>在这幅图中，当汇集一定个数的unseal keys&#8217;share后，vault就能够重构解封密钥(“unseal key”)，然后用它来解密得到根密钥(root key，也称为master key)，根密钥再被用来解密得到加密密钥(Encryption key)用于保护所有vault的数据，即这个Encryption key就是后续参与机密数据加解密的密钥。</p>
<blockquote>
<p>注：实际生产部署时，究竟要如何对Vault Server进行unseal，<a href="https://developer.hashicorp.com/vault/tutorials/recommended-patterns/pattern-unseal">HashiCorp提供了一些unseal pattern</a>供大家参考。</p>
</blockquote>
<ul>
<li>加密层</li>
</ul>
<p>前面架构图中左侧南北横贯多层的部分是Vault的加密层，被称为<strong>barrier</strong>，负责对Vault数据进行加密和解密，确保数据在存储和传输过程中的机密性和完整性。Vault服务器启动时，会将数据写入存储后端。由于存储后端位于barrier之外，被视为不可信的(与零信任网络理念一致)，因此Vault会在将数据发送到存储后端之前对其进行加密。这种机制确保了即便恶意攻击者试获取了对存储后端的访问权限，其拿到的数据仍然保持加密状态。</p>
<ul>
<li>认证和授权</li>
</ul>
<p>如下图(来自Hashicorp官方文档)，当客户端首次连接到Vault时，需要进行身份验证。Vault提供可配置的认证方法，并在身份验证机制上提供灵活性。操作员可以使用用户名/密码等机制进行身份验证，而应用程序可以使用公钥/私钥或令牌进行身份验证。 Core（核心）负责管理请求的流程，包括流经哪个身份验证方法来确定请求是否有效，并得到关联策略的列表，执行访问控制规则（ACLs），确保审计日志记录，并将请求路由到相应的机密引擎进行处理。</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-secret-management-by-example-5.png" alt="" /></p>
<ul>
<li>策略管理</li>
</ul>
<p>策略是一组命名的访问控制规则。Vault内置了一些策略，如”root”策略，允许对所有资源的访问。用户可以创建任意数量的命名策略，并对路径进行细粒度的控制。除非通过策略明确授权，否则不允许进行操作。</p>
<ul>
<li>机密引擎</li>
</ul>
<p>Vault使用机密引擎来生成和管理动态机密数据，如临时凭据、API密钥等。机密引擎的类型可以是静态的，如数据库凭据，也可以是动态的，如 AWS IAM凭据。机密引擎根据配置的规则和策略生成和提供机密数据。</p>
<ul>
<li>审计和日志记录</li>
</ul>
<p>Vault记录请求和响应的审计日志，并有Audit Broker(审计代理)将其分发到配置的审计设备(audit device)。审计日志用于监控和审计对Vault的访问和操作。</p>
<ul>
<li>Expiration Manager（租期管理）</li>
</ul>
<p>Vault由Expiration Mgr管理令牌和机密数据的过期，自动回收已过期的客户端令牌和机密数据。</p>
<ul>
<li>Token Store（令牌存储）</li>
</ul>
<p>Token Store生成和管理客户端令牌，用于进行后续的请求操作。令牌类似于网站登录时发送的 cookie，用于验证客户端的身份和授权。</p>
<p>以上是Vault的主要架构设计思路和各部分的功能范围。Vault的架构保证了安全性、高可用性和可扩展性，使用户能够安全地管理和保护机密信息。</p>
<h3>3.2 Vault的安全模型</h3>
<p>Vault是做机密信息管理的，其自身安全模型是否完善直接关系到应用系统的安全。Vault官方也十分重视这点，在<a href="https://developer.hashicorp.com/vault/docs/internals/security">官方文档中也对其安全模型做了说明</a>，这里梳理一下。</p>
<p>Vault的安全模型旨在提供数据的机密性、完整性、可用性、可追溯性和认证性。以下是Vault安全模型的几个设计要点：</p>
<ul>
<li>通信安全</li>
</ul>
<p>Vault要求客户端与服务器之间的通信通过TLS建立安全通道，以确保通信的机密性和完整性。此外，Vault服务器之间的集群通信也使用相互认证的TLS，以保护数据在传输过程中的机密性和完整性。</p>
<ul>
<li>身份验证和授权</li>
</ul>
<p>前面说架构时提及过：所有客户端请求必须经过适当的身份验证和授权。当客户端首次进行身份验证时，Vault使用认证方法验证客户端的身份，并返回与其关联的ACL策略列表。每个请求都需要提供有效的客户端令牌，Vault根据令牌验证其有效性，并生成基于关联策略的访问控制列表（ACL）。</p>
<ul>
<li>数据安全</li>
</ul>
<p>Vault对于存储在后端的数据，以及在传输过程中的数据，都要求保证安全。Vault使用256位的高级加密标准（AES）密码和96位随机数作为加密密钥，对离开Vault的所有数据进行加密。同时，在解密过程中验证Galios Counter Mode（GCM）的认证标签，以检测任何篡改。</p>
<ul>
<li>内部威胁保护</li>
</ul>
<p>Vault关注内部攻击威胁，即已经获得某种程度Vault访问权限的攻击者企图获取未经授权的机密信息。Vault在客户端进行身份验证时，使用事先配置的关联策略列表来生成客户端令牌，并使用严格的默认拒绝策略来进行访问控制。每个策略指定对Vault中路径的访问级别，最终的访问权限由所有关联策略中最高级别的权限决定。</p>
<ul>
<li>密钥管理</li>
</ul>
<p>Vault使用Shamir&#8217;s Secret Sharing技术来实现密钥的管理和保护unseal key，本质上也是对Root key和Encryption key的保护。只有在提供足够数量的share时，才能恢复unseal密钥，这样可以避免对单个持有者的绝对信任，同时也不需要存储完整的加密密钥。</p>
<p>但需要注意的是，Vault的安全模型并不涵盖所有可能的威胁和攻击，例如对存储后端的完全控制、存储后端中存在的秘密信息的泄露、运行中的Vault实例内存分析等。此外，Vault还依赖于外部系统或服务的安全性，如果这些外部系统存在漏洞或受到攻击，可能会导致Vault中数据的机密性或完整性受到威胁。</p>
<p>说了这么多Vault，Vault究竟长什么样？应该如何用呢？接下来我们简单介绍一下Vault的安装和使用，也是为后续的实例部分做个铺垫。</p>
<h3>3.3 Vault的安装</h3>
<p><a href="https://developer.hashicorp.com/vault/docs/install">Vault支持多种形式的安装部署</a>，包括基于预编译好的二进制文件(precompiled binary)、基于容器或包管理器等，你甚至可以自己基于源码编译。</p>
<p>我这里使用的是Precompiled binary方式，将Vault直接部署在我的开发环境下，一台MacBook Pro上。</p>
<p>Precompiled binary下载后就是一个可执行文件，把它放到特定路径下，并在PATH环境变量中将这个路径加入进来，环境变量生效后，你就可以在任意路径下使用vault命令了。</p>
<p>下面的命令打印了下载的vault的版本：</p>
<pre><code>$vault -v
Vault v1.15.1 (b94e275f25ccd9011146d14c00ea9e49fd5032dc), built 2023-10-20T19:16:11Z
</code></pre>
<p>通过-h命令行参数，可以查看vault的命令帮助信息：</p>
<pre><code>$vault -h
Usage: vault &lt;command&gt; [args]

Common commands:
    read        Read data and retrieves secrets
    write       Write data, configuration, and secrets
    delete      Delete secrets and configuration
    list        List data or secrets
    login       Authenticate locally
    agent       Start a Vault agent
    server      Start a Vault server
    status      Print seal and HA status
    unwrap      Unwrap a wrapped secret

Other commands:
    audit                Interact with audit devices
    auth                 Interact with auth methods
    debug                Runs the debug command
    events
    kv                   Interact with Vault's Key-Value storage
    lease                Interact with leases
    monitor              Stream log messages from a Vault server
    namespace            Interact with namespaces
    operator             Perform operator-specific tasks
    patch                Patch data, configuration, and secrets
    path-help            Retrieve API help for paths
    pki                  Interact with Vault's PKI Secrets Engine
    plugin               Interact with Vault plugins and catalog
    policy               Interact with policies
    print                Prints runtime configurations
    proxy                Start a Vault Proxy
    secrets              Interact with secrets engines
    ssh                  Initiate an SSH session
    token                Interact with tokens
    transform            Interact with Vault's Transform Secrets Engine
    transit              Interact with Vault's Transit Secrets Engine
    version-history      Prints the version history of the target Vault server
</code></pre>
<blockquote>
<p>注：Vault继承了Hashicorp产品的一贯风格，即将所有功能放到一个程序中，各个功能通过subcommand的形式提供，比如vault server、vault agent、vault proxy等。如果你了解consul，你会发现consul就是这样的。</p>
</blockquote>
<h3>3.4 Vault的启动(dev模式)</h3>
<p>生产环境的Vault部署、配置、启动以及unseal过程还是蛮复杂的，<a href="https://developer.hashicorp.com/vault/tutorials/day-one-consul/multi-cluster-architecture">HashiCorp给了一些参考集群架构</a>，这些可以交给运维同学去琢磨。</p>
<p>对于开发人员而言，日常将应用与Vault集成实现机密管理的时候，只需在本机或远程开发机上启动dev模式的Vault实例即可，这里我们也基于dev模式来启动一个单实例的Vault：</p>
<pre><code>$vault server -dev
==&gt; Vault server configuration:

Administrative Namespace:
             Api Address: http://127.0.0.1:8200
                     Cgo: disabled
         Cluster Address: https://127.0.0.1:8201
   Environment Variables: Apple_PubSub_Socket_Render, CLASSPATH, CLISH_PATH, ETCDCTL_API, GITEA_WORK_DIR, GODEBUG, GONOPROXY, GONOSUMDB, GOPATH, GOPRIVATE, GOPROXY, GOROOT, GOSUMDB, HOME, HOMEBREW_BOTTLE_DOMAIN, LANG, LC_CTYPE, LESS, LOGNAME, LSCOLORS, MML_HOME, NVM_BIN, NVM_CD_FLAGS, NVM_DIR, OLDPWD, OPENCV_PATH, PAGER, PATH, PWD, PYTHONPATH, RUSTUP_DIST_SERVER, RUSTUP_UPDATE_ROOT, SHELL, SHLVL, SSH_AUTH_SOCK, TERM, TERM_PROGRAM, TERM_PROGRAM_VERSION, TERM_SESSION_ID, TMPDIR, USER, XPC_FLAGS, XPC_SERVICE_NAME, ZSH, _
              Go Version: go1.21.3
              Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level:
                   Mlock: supported: false, enabled: false
           Recovery Mode: false
                 Storage: inmem
                 Version: Vault v1.15.1, built 2023-10-20T19:16:11Z
             Version Sha: b94e275f25ccd9011146d14c00ea9e49fd5032dc

==&gt; Vault server started! Log data will stream in below:

2023-11-06T10:25:37.723+0800 [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
2023-11-06T10:25:37.727+0800 [INFO]  incrementing seal generation: generation=1
2023-11-06T10:25:37.727+0800 [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2023-11-06T10:25:37.733+0800 [INFO]  core: Initializing version history cache for core
2023-11-06T10:25:37.734+0800 [INFO]  events: Starting event system
2023-11-06T10:25:37.736+0800 [INFO]  core: security barrier not initialized
2023-11-06T10:25:37.737+0800 [INFO]  core: security barrier initialized: stored=1 shares=1 threshold=1
2023-11-06T10:25:37.744+0800 [INFO]  core: post-unseal setup starting
2023-11-06T10:25:37.758+0800 [INFO]  core: loaded wrapping token key
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T10:25:37.758+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T10:25:37.760+0800 [INFO]  core: no mounts; adding default mount table
2023-11-06T10:25:37.765+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T10:25:37.774+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T10:25:37.777+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T10:25:37.783+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T10:25:37.785+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T10:25:37.787+0800 [INFO]  rollback: starting rollback manager
2023-11-06T10:25:37.789+0800 [INFO]  core: restoring leases
2023-11-06T10:25:37.791+0800 [INFO]  identity: entities restored
2023-11-06T10:25:37.791+0800 [INFO]  identity: groups restored
2023-11-06T10:25:37.791+0800 [INFO]  expiration: lease restore complete
2023-11-06T10:25:37.793+0800 [INFO]  core: Recorded vault version: vault version=1.15.1 upgrade time="2023-11-06 02:25:37.793171 +0000 UTC" build date=2023-10-20T19:16:11Z
2023-11-06T22:25:38.367+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.368+0800 [INFO]  core: root token generated
2023-11-06T22:25:38.368+0800 [INFO]  core: pre-seal teardown starting
2023-11-06T22:25:38.369+0800 [INFO]  rollback: stopping rollback manager
2023-11-06T22:25:38.369+0800 [INFO]  core: pre-seal teardown complete
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener.tcp: starting listener: listener_address=127.0.0.1:8201
2023-11-06T22:25:38.370+0800 [INFO]  core.cluster-listener: serving cluster requests: cluster_listen_address=127.0.0.1:8201
2023-11-06T22:25:38.371+0800 [INFO]  core: post-unseal setup starting
2023-11-06T22:25:38.371+0800 [INFO]  core: loaded wrapping token key
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin runtime catalog
2023-11-06T22:25:38.371+0800 [INFO]  core: successfully setup plugin catalog: plugin-directory=""
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=system version="v1.15.1+builtin.vault" path=sys/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=identity version="v1.15.1+builtin.vault" path=identity/ namespace="ID: root. Path: "
2023-11-06T22:25:38.372+0800 [INFO]  core: successfully mounted: type=cubbyhole version="v1.15.1+builtin.vault" path=cubbyhole/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  core: successfully mounted: type=token version="v1.15.1+builtin.vault" path=token/ namespace="ID: root. Path: "
2023-11-06T22:25:38.373+0800 [INFO]  rollback: Starting the rollback manager with 256 workers
2023-11-06T22:25:38.373+0800 [INFO]  rollback: starting rollback manager
2023-11-06T22:25:38.374+0800 [INFO]  core: restoring leases
2023-11-06T22:25:38.374+0800 [INFO]  expiration: lease restore complete
2023-11-06T22:25:38.374+0800 [INFO]  identity: entities restored
2023-11-06T22:25:38.374+0800 [INFO]  identity: groups restored
2023-11-06T22:25:38.374+0800 [INFO]  core: post-unseal setup complete
2023-11-06T22:25:38.374+0800 [INFO]  core: vault is unsealed
2023-11-06T22:25:38.386+0800 [INFO]  core: successful mount: namespace="" path=secret/ type=kv version=""
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variables:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: KiF1ohtchsOjr4IvzHY38/OAPOqS1/rARczTFG6Ull8=
Root Token: hvs.9QOJsa7zlwHO8ieW15CXXoOp

Development mode should NOT be used in production installations!
</code></pre>
<p>我们看到dev模式下，Vault server是自动unseal的，并打印出了Unseal Key和Root Token，而且显式地告诉你：所有机密数据都是存储在内存中的，<strong>不要将这个模式用于生产环境</strong>。</p>
<p>前面说过，vault程序继承了Hashicorp产品的基因，它既可以用来启动server，其自身也是一个命令行程序，我们可以用vault命令查看启动的server的状态：</p>
<pre><code>$vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": http: server gave HTTP response to HTTPS client
</code></pre>
<p>我们看到：获取vault server状态的命令执行失败，因为我们并没有开启vault server的https端口，仅使用了http端口。我们设置一下环境变量后，再执行status命令：</p>
<pre><code>$export VAULT_ADDR='http://127.0.0.1:8200' // 设置vault server addr为http非安全方式
$vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.15.1
Build Date      2023-10-20T19:16:11Z
Storage Type    inmem
Cluster Name    vault-cluster-23f54192
Cluster ID      a86c14e2-b88c-5391-e8b4-0b1b9e9a9aaf
HA Enabled      false
</code></pre>
<p>接下来，我们试着向Vault写入一个机密信息。Vault支持多种secret engine，比如：<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/static-secrets">Key/Value secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/versioned-kv">Versioned Key/value secrets engine(k/v引擎的v2版本)</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/openldap">LDAP secrets engine</a>、<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/azure-secrets">Azure secrets engine</a>等，其中K/V引擎以及带版本的K/V引擎是最常用的。</p>
<blockquote>
<p>注：Vault还支持开发者<a href="https://developer.hashicorp.com/vault/tutorials/custom-secrets-engine">自定义secret engine</a>。</p>
</blockquote>
<p>我们尝试使用kv子命令向vault中写入一个key/value，放到secret路径下(在dev模式下，secret路径下自动开启v2版本引擎)，key为hello，值为foo=world：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
Error making API request.

URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret
Code: 403. Errors:

* permission denied

</code></pre>
<p>我们看到命令执行失败，提示没有权限。vault server要求每个访问请求都必须带上token，我们可以使用vault server启动时打印的root token，可以使用环境变量的方式将token注入：</p>
<pre><code>export VAULT_TOKEN="hvs.9QOJsa7zlwHO8ieW15CXXoOp"
</code></pre>
<p>也可以执行下面命令并输入root token完成登录：</p>
<pre><code>$vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.9QOJsa7zlwHO8ieW15CXXoOp
token_accessor       170OHOscEZjfl8fSa8aVpNkZ
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
</code></pre>
<p>之后，root token就被放置在“~/.vault-token”中了：</p>
<pre><code>$cat ~/.vault-token
hvs.9QOJsa7zlwHO8ieW15CXXoOp
</code></pre>
<blockquote>
<p>注：我们通常不会使用root token，而是会<a href="https://developer.hashicorp.com/vault/tutorials/secrets-management/vault-nomad-secrets">利用vault token命令生成新token</a>作为vault cli访问vault server的token。</p>
</blockquote>
<p>现在我们重新执行一下kv put命令：</p>
<pre><code>$vault kv put -mount=secret hello foo=world
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2
</code></pre>
<p>kv创建成功，路径secret/data/hello(注：vault会默认在mount的路径secret下创建data路径)。vault server在将value值存储在backend storage(这里是memory)前，会用Encryption Key对内容进行加密。如果你多执行几次这个命令，你会发现输出信息中的version的数值会递增，这个数值表示设置的值的版本。</p>
<p>我们可以用kv get获取刚才写入的kv值，vault会将数据从backend storage中读取出来并解密：</p>
<pre><code>$vault kv get -mount=secret hello
== Secret Path ==
secret/data/hello

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-06T03:01:25.968883Z
custom_metadata    &lt;nil&gt;
deletion_time      n/a
destroyed          false
version            2

=== Data ===
Key    Value
---    -----
foo    world
</code></pre>
<p>我们还可以通过delete删除刚刚建立的kv值(为后面的基本场景示例做铺垫)：</p>
<pre><code>$vault kv delete secret/foo
Success! Data deleted (if it existed) at: secret/data/foo
$vault kv get secret/foo
No value found at secret/data/foo
</code></pre>
<p>到这里我们看到，一旦vault安装完毕后，基本使用场景还是蛮简单的，但也仅限于基本使用场景^_^。下面我们再来看看如何通过代码来实现这些基本功能场景。</p>
<h3>3.5 使用client SDK与Vault交互</h3>
<p><a href="https://developer.hashicorp.com/vault/api-docs/libraries">Vault支持各种主流语言的client SDK</a>，其中Vault官方维护了三个：Go、Ruby和C&#35;，其他语言的SDK则是由社区维护。</p>
<p>我们用Go Client SDK来编写一个设置kv和获取kv值的小程序，如下面代码所示：</p>
<pre><code>// secret-management-examples/basic/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
)

func main() {
    // 创建一个新的Vault客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Println("无法创建Vault客户端:", err)
        return
    }

    // 设置Vault服务器的地址
    client.SetAddress("http://localhost:8200/")

    // 设置Vault的访问令牌（如果需要认证）
    client.SetToken("hvs.9QOJsa7zlwHO8ieW15CXXoOp")

    // 设置要写入的机密信息
    secretData := map[string]interface{}{
        "foo": "bar",
    }

    kv2 := client.KVv2("secret") // mount "secret"

    // 写入机密信息到Vault的secret/data/{key}路径下
    key := "hello"
    _, err = kv2.Put(context.Background(), key, secretData)
    if err != nil {
        fmt.Println("无法写入机密信息:", err)
        return
    }

    // 读取Vault的secret/data/{key}路径下的机密信息
    secret, err := kv2.Get(context.Background(), key)
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", secret.Data)
}
</code></pre>
<p>我们看到：默认创建的api.Client操作的都是v1版本的数据，这里通过KVv2方法将其转换为可以操作v2版本数据的client，之后put和get就可以如预期正常工作了！</p>
<p>下面是其运行结果：</p>
<pre><code>$go run main.go
读取到的值: map[foo:bar]
</code></pre>
<p>有了基础场景做铺垫，接下来我们就进入实例环节，看看应用是如何基于Vault应对一些常见的机密管理场景的。</p>
<h2>4. 常见的机密管理场景</h2>
<p>Vault支持对多种机密信息的管理，包括应用访问外部服务或资源所需的用户名/密码、API密钥或访问令牌(token)，应用程序的配置中的机密配置信息，比如数据库连接字符串、加密密钥等，以及私钥、证书等加密相关的机密信息等。这里我们就分别来看看应用与Vault集成并获取这些机密信息的场景，不过在这之前，我们首先需要先来了解一下应用本身与Vault是如何集成的。</p>
<h3>4.1 应用通过Vault身份认证和授权的方法</h3>
<p>在3.5小节的基本场景示例中，我们的client使用了<a href="https://developer.hashicorp.com/vault/docs/auth/token">一个长期有效的token</a>通过了Vault的身份认证和授权环节，拥有了操作Vault数据的权限。</p>
<p>token auth方法也是dev模式下Vault server实例支持的唯一auth method，我们可以通过auth list命令查看vault server当前支持的auth方法集合：</p>
<pre><code>$vault auth list
Path      Type     Accessor               Description                Version
----      ----     --------               -----------                -------
token/    token    auth_token_6f9cc41c    token based credentials    n/a
</code></pre>
<p>不过，基于token来实现app与Vault的集成并非Vault官方推荐的在生产环境使用的<a href="https://developer.hashicorp.com/vault/docs/auth">auth方式</a>，理由也很明显：这种方式涉及手动创建一个长期有效的令牌，这有悖于最佳实践，并存在安全风险。</p>
<p>除了Token auth method，Vault还支持<a href="https://developer.hashicorp.com/vault/docs/auth/approle">AppRole</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/jwt">JWT/OIDC</a>、<a href="https://developer.hashicorp.com/vault/docs/auth/cert">TLS证书</a>以及<a href="https://developer.hashicorp.com/vault/docs/auth/userpass">User/Password</a>等<a href="https://developer.hashicorp.com/vault/docs/auth">多种auth method</a>，这些auth method的共同之处在于通过身份认证后，Vault可自动创建<strong>短期令牌</strong>供客户端使用，无需定期手动生成新令牌，短期令牌可以减少令牌泄露的风险，因为短期令牌在一定时间后会自动失效，并需要重新进行身份认证。</p>
<p>简单起见，我这里就用User/Password method作为实例演示一下应用通过Vault的身份认证和授权。</p>
<p>我们先来开启(enable)基于User/Password的auth method：</p>
<pre><code>$vault auth enable userpass
Success! Enabled userpass auth method at: userpass/
</code></pre>
<p>该命令默认将会启用auth/userpass路径，之后通过auth list查看，就能在list中看到新增的userpass auth method了：</p>
<pre><code>$vault auth list
Path         Type        Accessor                  Description                Version
----         ----        --------                  -----------                -------
token/       token       auth_token_6f9cc41c       token based credentials    n/a
userpass/    userpass    auth_userpass_b5b6e974    n/a                        n/a

</code></pre>
<p>接下来，我们在vault服务实例中建立一个新的user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [default]
token_ttl                  0s
token_type                 default
</code></pre>
<p>下面是示例代码：</p>
<pre><code>// secret-management-examples/auth_user_password/main.go

package main

import (
    "context"
    "fmt"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>如果你在Vault的GO SDK中没有找到对user/password auth method的直接支持，你也可以参考<a href="https://developer.hashicorp.com/vault/api-docs/auth/userpass">user/password auth method的API文档</a>自行实现登录Vault并读取特定机密信息，代码如下(与上面代码功能是等价的)：</p>
<pre><code>// secret-management-examples/auth_user_password_self_impl/main.go

func clientAuth(vaultAddr, user, pass string) (*api.Secret, error) {
    payload := fmt.Sprintf(`{"password": "%s"}`, pass)

    req, err := http.NewRequest("POST", vaultAddr+"/v1/auth/userpass/login/"+user, strings.NewReader(payload))
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New(string(body))
    }

    return api.ParseSecret(bytes.NewReader(body))
}

func main() {
    vaultAddr := "http://localhost:8200"
    user := "tonybai"
    pass := "ilovegolang"

    // client登录vault服务器获取临时访问令牌
    secret, err := clientAuth(vaultAddr, user, pass)
    if err != nil {
        fmt.Printf("登录Vault失败: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }

    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")
    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    kv2 := client.KVv2("secret") // mount "secret"
    // 读取Vault的secret/data/{key}路径下的机密信息
    data, err := kv2.Get(context.Background(), "hello")
    if err != nil {
        fmt.Println("无法读取机密信息:", err)
        return
    }

    // 打印读取到的值
    fmt.Println("读取到的值:", data.Data)
}
</code></pre>
<p>我们运行一下上述两个示例代码之一：</p>
<pre><code>$go run main.go
登录Vault成功
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* 1 error occurred:
    * permission denied

</code></pre>
<p>通过错误信息来看，“tonybai”这个user没有权限读取secret/data/hello下的机密信息！那么怎么给这个用户加上secret/data/hello的读取权限呢？Vault通过policy来管理权限，如果某个user具有某个policy的绑定，那么该user就拥有该policy设定的权限，这有点像RBAC的思路，只是没有引入role的概念! 我们先来添加一个拥有secret/data/hello读权限的policy：</p>
<pre><code>$vault policy write my-policy -&lt;&lt;EOF
# Allow "read" permission on "secret/data/*" secrets
path "secret/data/*" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: my-policy
</code></pre>
<p>接下来重写user的属性数据，将my-policy赋给”tonybai”这个user：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  0s
token_type                 default
</code></pre>
<p>完成上述设置后，我们再来运行一下基于user/password auth method的程序：</p>
<pre><code>$go run main.go
登录Vault成功
读取到的值: map[foo:bar]
</code></pre>
<p>这次程序成功登录Vault并成功读取了secret/data/hello下面的机密数据。</p>
<p>这里我们除了设置了token_policies，其他属性都保持了默认值，这样我们拿到的临时token其实并不“临时”，我们可以一直使用。下面我们通过设置token_ttl来指定每个临时token的最大有效时间：</p>
<pre><code>$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy token_ttl=5s
Success! Data written to: auth/userpass/users/tonybai

$vault read auth/userpass/users/tonybai
Key                        Value
---                        -----
token_bound_cidrs          []
token_explicit_max_ttl     0s
token_max_ttl              0s
token_no_default_policy    false
token_num_uses             0
token_period               0s
token_policies             [my-policy]
token_ttl                  5s
token_type                 default
</code></pre>
<p>我们改写一下程序，让程序每隔1秒用临时token获取一下机密信息并输出：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go (临时版本)

    for {
        // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
        data, err := kv2.Get(context.Background(), "hello")
        if err != nil {
            fmt.Println("无法读取机密信息:", err)
            return
        }

        // 打印读取到的值
        log.Println("读取到的值:", data.Data)
        time.Sleep(time.Second)
    }
</code></pre>
<p>我们运行这个程序将得到如下结果：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:24:17 读取到的值: map[foo:bar]
2023/11/06 05:24:18 读取到的值: map[foo:bar]
2023/11/06 05:24:19 读取到的值: map[foo:bar]
2023/11/06 05:24:20 读取到的值: map[foo:bar]
2023/11/06 05:24:21 读取到的值: map[foo:bar]
无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request.

URL: GET http://localhost:8200/v1/secret/data/hello
Code: 403. Errors:

* permission denied
</code></pre>
<p>我们看到如果token过期，而我们的程序又没有<a href="https://developer.hashicorp.com/vault/docs/concepts/lease#lease-durations-and-renewal">对token进行续期(renewal)</a>，程序后续对Vault中机密数据的访问将以”permission denied”的失败而告终。下面我们就来为程序加上token续期，Vault SDK提供了LifetimeWatcher来辅助token续期工作，下面就是利用LifetimeWatcher进行token续期的示例：</p>
<pre><code>// secret-management-examples/auth_user_password_renewal/main.go

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/hashicorp/vault/api"
    auth "github.com/hashicorp/vault/api/auth/userpass"
)

func main() {
    user := "tonybai"
    pass := "ilovegolang"

    // 创建Vault API客户端
    client, err := api.NewClient(api.DefaultConfig())
    if err != nil {
        fmt.Printf("无法创建Vault客户端: %v\n", err)
        return
    }
    // 设置 Vault 地址
    client.SetAddress("http://localhost:8200")

    // client登录vault服务器获取临时访问令牌
    userpassAuth, err := auth.NewUserpassAuth(user, &amp;auth.Password{FromString: pass})
    if err != nil {
        fmt.Errorf("无法初始化userpass auth method: %w", err)
        return
    }

    secret, err := client.Auth().Login(context.Background(), userpassAuth)
    if err != nil {
        fmt.Errorf("登录Vault失败: %w", err)
        return
    }
    if secret == nil {
        fmt.Printf("登录后没有secret信息返回: %v\n", err)
        return
    }
    fmt.Printf("登录Vault成功\n")

    token := secret.Auth.ClientToken

    // 设置临时访问令牌
    client.SetToken(token)

    // 设置renewel watcher
    watcher, err := client.NewLifetimeWatcher(&amp;api.LifetimeWatcherInput{
        Secret: secret,
    })
    go watcher.Start()
    defer watcher.Stop()

    kv2 := client.KVv2("secret") // mount "secret"
    ticker := time.NewTicker(time.Second)

    for {
        select {
        case err := &lt;-watcher.DoneCh():
            if err != nil {
                log.Printf("Failed to renew token: %v. Re-attempting login.", err)
                return
            }

            // This occurs once the token has reached max TTL.
            log.Printf("Token can no longer be renewed. Re-attempting login.")
            return

        case renewal := &lt;-watcher.RenewCh():
            // Renewal is now over
            log.Printf("Successfully renewed: %#v", renewal)

        case &lt;-ticker.C:
            // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息
            data, err := kv2.Get(context.Background(), "hello")
            if err != nil {
                fmt.Println("无法读取机密信息:", err)
                continue
            }
            // 打印读取到的值
            log.Println("读取到的值:", data.Data)
        }
    }
}

</code></pre>
<p>运行上述示例(此时token_ttl为5s)：</p>
<pre><code>$go run main.go
登录Vault成功
2023/11/06 05:17:42 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 42, 233750000, time.UTC), Secret:(*api.Secret)(0xc000114a80)}
2023/11/06 05:17:43 读取到的值: map[foo:bar]
2023/11/06 05:17:44 读取到的值: map[foo:bar]
2023/11/06 05:17:45 读取到的值: map[foo:bar]
2023/11/06 05:17:45 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 45, 841374000, time.UTC), Secret:(*api.Secret)(0xc0002827e0)}
2023/11/06 05:17:46 读取到的值: map[foo:bar]
2023/11/06 05:17:47 读取到的值: map[foo:bar]
2023/11/06 05:17:48 读取到的值: map[foo:bar]
2023/11/06 05:17:49 读取到的值: map[foo:bar]
2023/11/06 05:17:49 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 49, 443211000, time.UTC), Secret:(*api.Secret)(0xc0002831a0)}
2023/11/06 05:17:50 读取到的值: map[foo:bar]
2023/11/06 05:17:51 读取到的值: map[foo:bar]
2023/11/06 05:17:52 读取到的值: map[foo:bar]
2023/11/06 05:17:53 Successfully renewed: &amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 53, 46880000, time.UTC), Secret:(*api.Secret)(0xc000115a40)}
2023/11/06 05:17:53 读取到的值: map[foo:bar]
2023/11/06 05:17:54 读取到的值: map[foo:bar]
... ...
</code></pre>
<p>我们看到，在token过期之前，LifetimeWatcher帮助Client完成了续期请求。LifetimeWatcher运行在一个单独的goroutine中，通过channel与main goroutine通信。Vault默认token_max_ttl的值为32天，即便你没有设置其值，当token续期到32天时，就无法再renew了，此时watcher.DoneCh会返回事件，这是让你重新login的信号，示例中只给出了注释，并未重新login，大家注意一下。出于安全考虑，可以将token_max_ttl设置为一个合理的值，使其起到应有的安全作用。</p>
<p>通过这个示例我们看到，只要通过Vault的身份认证和授权，我们就能安全地存储和使用机密信息了。那么如何保证应用在与Vault进行身份认证和授权时所使用的凭据的安全呢？比如上面程序里所需的user和password。这个感觉又回到“先有鸡还是先有蛋”的问题了！实际在生产环境，我们可以依赖IaaS层或公有云的安全措施来保证，比如通过环境变量在运行时注入user和password；再比如利用公有云提供的KMS(key management system)或HSM(Hardware Security Module)服务来保证user和password安全。</p>
<h3>4.2  静态secret</h3>
<p>将静态secret作为机密信息保存和管理，是Vault非常常见的应用。secret可以存在很长时间不变，或可能很少改变。Vault可以使用它的加密屏障(barrier)存储这些secret，应用程序运行时可以向Vault请求读取这些secret来使用。</p>
<p>Vault的versioned secrets engine支持你以安全的方式存储和管理secret，同时还提供secret的版本控制能力。你可以使用不同版本的secret进行应用程序升级或回滚，也可以在需要时轻松地恢复旧版本secret。引擎还可以记录secret每个版本的修改人和修改时间。</p>
<p>关于静态secret的管理和使用，可以参见3.5中的基本场景，这里就不赘述了。</p>
<h3>4.3 动态secret</h3>
<p>有静态、长有效期的静态secret，就会有对应的动态secret。和静态secret相比，动态secret安全性高，每个动态secret的有效期都较短，并且一旦泄露可以马上撤销，同时动态secret也便于轮换，定期自动过期无需中断业务。</p>
<p>Vault提供了对多种针对不同系统的动态secret管理能力，包括数据库访问凭据、Active Directory账号, SSH keys和PKI certificates ，Vault针对不同系统提供了不同的secret engine。</p>
<p>Vault官方举了一个有关<a href="https://developer.hashicorp.com/vault/tutorials/db-credentials/database-secrets">使用Database Secrets Engine实现数据库动态secret</a>的示例，</p>
<p>鉴于篇幅，这里也不细说了。</p>
<h3>4.4 其他场景</h3>
<p>根据<a href="https://developer.hashicorp.com/vault/docs/use-cases">Vault官方文档对Vault应用场景的描述</a>，除了静态和动态secret类机密信息，Vault可以处理以下类型的机密信息：</p>
<ul>
<li>数据加密类(Data encryption)机密信息 </li>
</ul>
<p>Vault支持将数据加密服务外包给Vault，应用只需关注数据的加密与解密，Vault负责核心密钥和加密管理。Vault还支持对数据进行传输加密与存储加密。</p>
<ul>
<li>身份识别类(Identity-Based access)机密信息</li>
</ul>
<p>Vault支持从不同身份验证系统整合用户身份，实现统一的ACL系统，管理对系统和应用的访问。</p>
<ul>
<li>加密密钥类(Key management)机密信息</li>
</ul>
<p>Vault支持对云提供商密钥的生命周期管理，例如管理AWS KMS或GCP云密钥。</p>
<p>鉴于篇幅和实验环境有限，这里就针对每种情况做详细示例说明了，大家可以根据自己的需求，针对具体的某个场景做专题性的研究。</p>
<h2>5. 小结</h2>
<p>本文首先介绍了机密管理的概念，阐述了在现代Web应用开发中，为何需要重视机密管理。</p>
<p>接着，文中概述了专用于实现机密管理的机密管理系统的发展历程，以及从功能上逐步演化出的云原生机密管理系统的特征。</p>
<p>文章以业内知名的开源机密管理系统HashiCorp Vault为例，全面系统地介绍了它的架构设计、安全模型、使用方法，并详细阐释了应用程序如何通过与Vault API/SDK的集成，实现对各类机密信息的安全存储、动态生成、访问控制、审计等功能。</p>
<p>最后，文章用代码实例详细演示了基于Vault的几个典型机密管理场景，如不同类型机密信息的读写操作，以及不同认证方式的集成等。</p>
<p>这是个”每个人都应该重视安全的时代”，安全需要每个环节的参与，一处薄弱，就会导致“处处薄弱”。我相信本文的内容能有助于让大家对机密管理的概念、重要性及具体实现方法有更深入的理解。</p>
<p>本文涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/secret-management-examples">这里</a>下载。</p>
<blockquote>
<p>注：Vault项目还提供了Vault Agent和Vault Proxy，旨在为应用提供更可扩展、更简单的方式来集成Vault，消除应用程序采用Vault的初期障碍。Vault Agent可以获取secrets并将它们提供给应用程序，Vault Proxy可以在Vault和应用程序之间充当代理，可选地简化认证过程并缓存请求。有兴趣的童鞋可以参考<a href="https://developer.hashicorp.com/vault/docs/agent-and-proxy">Vault Agent和Proxy的官方文档</a>。</p>
</blockquote>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://arxiv.org/abs/2109.09905">Comparative Analysis of Cryptographic Key Management Systems</a> &#8211; https://arxiv.org/abs/2109.09905</li>
<li><a href="https://arxiv.org/abs/2208.11280">What are the Practices for Secret Management in Software Artifacts?</a> &#8211; https://arxiv.org/abs/2208.11280</li>
<li><a href="https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing">Shamir&#8217;s secret sharing</a> &#8211; https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing</li>
<li><a href="https://www.hashicorp.com/blog/vault-announcement">HashiCorp Vault</a> &#8211; https://www.hashicorp.com/blog/vault-announcement</li>
<li><a href="https://developer.hashicorp.com/vault/docs/internals/architecture">Vault Architecture</a> &#8211; https://developer.hashicorp.com/vault/docs/internals/architecture</li>
<li><a href="https://github.com/external-secrets/external-secrets">External Secrets</a> &#8211; https://github.com/external-secrets/external-secrets</li>
<li><a href="https://www.hashicorp.com/resources/5-best-practices-for-secrets-management">5 best practices for secrets management</a> &#8211; https://www.hashicorp.com/resources/5-best-practices-for-secrets-management</li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html">Secrets Management Cheat Sheet</a> &#8211; https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html</li>
<li><a href="https://www.imperva.com/learn/data-security/secret-management/">Secret Management</a> &#8211; https://www.imperva.com/learn/data-security/secret-management/</li>
<li><a href="https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices">Best practices for secrets management in Key Vault</a> &#8211; https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices</li>
<li><a href="https://www.beyondtrust.com/resources/glossary/secrets-management">Glossary: Secrets Management</a> &#8211; https://www.beyondtrust.com/resources/glossary/secrets-management</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过实例理解Go Web身份认证的几种方式</title>
		<link>https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/</link>
		<comments>https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/#comments</comments>
		<pubDate>Mon, 23 Oct 2023 13:49:26 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[2FA]]></category>
		<category><![CDATA[Admission]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[AuthN]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[AuthZ]]></category>
		<category><![CDATA[base64]]></category>
		<category><![CDATA[basicAuth]]></category>
		<category><![CDATA[CA]]></category>
		<category><![CDATA[Client]]></category>
		<category><![CDATA[Cookie]]></category>
		<category><![CDATA[CRUD]]></category>
		<category><![CDATA[CSRF]]></category>
		<category><![CDATA[Digest]]></category>
		<category><![CDATA[Form]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[header]]></category>
		<category><![CDATA[HMAC]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[jwt]]></category>
		<category><![CDATA[key]]></category>
		<category><![CDATA[MFA]]></category>
		<category><![CDATA[OAuth2]]></category>
		<category><![CDATA[OpenID]]></category>
		<category><![CDATA[OTP]]></category>
		<category><![CDATA[OWASP]]></category>
		<category><![CDATA[Passkey]]></category>
		<category><![CDATA[password]]></category>
		<category><![CDATA[payload]]></category>
		<category><![CDATA[PEM]]></category>
		<category><![CDATA[rfc]]></category>
		<category><![CDATA[Server]]></category>
		<category><![CDATA[session]]></category>
		<category><![CDATA[SHA256]]></category>
		<category><![CDATA[signature]]></category>
		<category><![CDATA[SSO]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[token]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[WebAuthn]]></category>
		<category><![CDATA[XSS]]></category>
		<category><![CDATA[一次性密码]]></category>
		<category><![CDATA[人脸识别]]></category>
		<category><![CDATA[会话]]></category>
		<category><![CDATA[信息论]]></category>
		<category><![CDATA[准入]]></category>
		<category><![CDATA[单点登录]]></category>
		<category><![CDATA[双因素认证]]></category>
		<category><![CDATA[声纹]]></category>
		<category><![CDATA[多因素认证]]></category>
		<category><![CDATA[客户端]]></category>
		<category><![CDATA[密码]]></category>
		<category><![CDATA[指纹]]></category>
		<category><![CDATA[无状态]]></category>
		<category><![CDATA[有状态]]></category>
		<category><![CDATA[服务端]]></category>
		<category><![CDATA[短信验证码]]></category>
		<category><![CDATA[签名]]></category>
		<category><![CDATA[虹膜]]></category>
		<category><![CDATA[表单]]></category>
		<category><![CDATA[身份认证]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4023</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/10/23/understand-go-web-authn-by-example 在2023年Q1 Go官方用户调查报告中，API/RPC services、Websites/web services都位于使用Go开发的应用类别的头部(如下图)： 我个人使用Go开发已很多年，但一直从事底层基础设施、分布式中间件等方向，Web应用开发领域涉及较少，像Web应用领域常见的CRUD更是少有涉猎，不能不说是一种“遗憾”^_^。未来一段时间，团队会接触到Web应用的开发，我打算对Go Web应用开发的重点环节做一个快速系统的梳理。 而身份认证(Authentication，简称AuthN)是Web应用开发中一个关键的环节，也是首个环节，它负责验证用户身份，让用户可以以认证过的身份访问系统中的资源和信息。 Go语言作为一门优秀的Web开发语言，提供了丰富的机制来实现Web应用的用户身份认证。在这篇文章中，我就通过Go示例和大家一起探讨一下当前Web应用开发中几种常见的主流身份认证方式，帮助自己和各位读者迈出Web应用开发修炼之路的第一步。 1. 身份认证简介 1.1 身份认证解决的问题 身份认证不局限于Web应用，各种系统都会有身份认证，但本文我们聚焦Web应用领域的身份认证技术。 几乎所有Web应用的安全性都是从身份认证开始的，身份认证是验证用户身份真实性的过程，是我们首先要部署的策略。位于下游的安全控制，如授权(Authorization, AuthZ)、审计日志(Audit log)等，几乎都需要用户的身份。 身份认证的英文是Authentication，简写为AuthN，大家不要将之与授权Authorization(AuthZ)混淆(在后续系列文章中会继续探讨AuthZ相关的内容)，他们所要解决的问题相似，但有不同，也有先后。通常先AuthN，再AuthZ。我们可以用下面的比喻来形象地解释二者的联系与差异: AuthN就像是进入公司大楼的安检，负责检查员工的身份是否合法，是否具有进入公司的资格，它解决的是验证员工身份的问题。 AuthZ更像是公司内部的权限管理，某个员工进入了公司后(AuthN后)想访问一些重要资料，这时还需要确认该员工是否有相应的访问权限。它解决的是授权访问控制的问题。 简单来说，AuthN是验证你是谁，authZ是验证你有哪些权限。AuthN解决认证问题，AuthZ解决授权问题，这两个都重要，AuthN解决外部的安全问题，authZ解决内部的安全与合规问题。 1.2 身份认证的三要素 身份认证需要被认证方提供一些身份信息输入，这些代表身份信息的输入被称为身份认证要素（authentication factor）。这些要素有很多，大致可分为三类： 你知道的东西(What you know) 即基于被认证方知道的特定信息来验证身份，最常见的如密码等。 你拥有的东西(What you have) 基于被认证方所拥有的特定物件来验证身份，最常见的利用数字证书、令牌卡等。N年前，在移动端应用还没有发展起来时，一些人在银行办理电子银行业务时会拿到一个U盾(又称为USBKey)，其中存放着用于用户身份识别的数字证书，这个U盾就属于此类要素。 上面比喻中进入大楼时使用的员工卡也属于这类要素。 你本身就具有的(What you are) 即基于被认证方所拥有的生物特征要素(biometric factor)来验证身份，最常见的人脸识别、指纹/声纹/虹膜识别和解锁等。理论上来说，具备个人生物特征的身份认证标志具有不可仿冒性、唯一性。 如果上面比喻中的大楼已经开启了人脸识别功能，那么基于人脸识别的认证就属于这类要素的认证。 通常我们会基于单个要素设计身份认证方案，一旦使用两个或两个以上不同类的要素，就可以被称为双因素认证(2FA)或多因素认证(MFA)了。不过，2FA和MFA都比较复杂，不再本篇文章讨论范围之内。 基于上述要素，我们就可以设计和实现各种适合不同类别Web应用或API服务的身份认证方法了。Web应用和API服务都需要身份认证，它们有什么差异呢？这些差异是否会对身份认证方案产生影响呢？我们接下来看一下。 1.3 Web应用身份认证 vs. API服务身份认证 Web应用和API服务主要有以下几点区别: 交互方式不同 Web应用是浏览器与服务器之间的交互，用户通过浏览器访问Web应用。而API服务是程序/应用与服务器之间的交互，通过API请求获取数据或执行操作。 返回数据格式不同 Web应用通常会返回html/js/css等浏览器可解析执行的代码，而API服务通常返回结构化数据，常见的如JSON或XML等。 使用场景不同 Web应用主要面向人类用户的使用，用户通过浏览器进行操作。而API服务主要被其他程序调用，为程序之间提供接口与数据支撑。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/10/23/understand-go-web-authn-by-example">本文永久链接</a> &#8211; https://tonybai.com/2023/10/23/understand-go-web-authn-by-example</p>
<p>在<a href="https://go.dev/blog/survey2023-q1-results">2023年Q1 Go官方用户调查报告</a>中，API/RPC services、Websites/web services都位于使用Go开发的应用类别的头部(如下图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-2.png" alt="" /></p>
<p>我个人使用Go开发已很多年，但一直从事底层基础设施、分布式中间件等方向，Web应用开发领域涉及较少，像Web应用领域常见的CRUD更是少有涉猎，不能不说是一种“遗憾”^_^。未来一段时间，团队会接触到Web应用的开发，我打算对Go Web应用开发的重点环节做一个快速系统的梳理。</p>
<p>而身份认证(Authentication，简称AuthN)是Web应用开发中一个关键的环节，也是首个环节，它负责验证用户身份，让用户可以以认证过的身份访问系统中的资源和信息。</p>
<p>Go语言作为一门优秀的Web开发语言，提供了丰富的机制来实现Web应用的用户身份认证。在这篇文章中，我就通过Go示例和大家一起探讨一下当前Web应用开发中几种常见的主流身份认证方式，帮助自己和各位读者<strong>迈出Web应用开发修炼之路的第一步</strong>。</p>
<h2>1. 身份认证简介</h2>
<h3>1.1 身份认证解决的问题</h3>
<p>身份认证不局限于Web应用，各种系统都会有身份认证，但本文我们聚焦Web应用领域的身份认证技术。</p>
<p>几乎所有Web应用的安全性都是从身份认证开始的，身份认证是验证用户身份真实性的过程，是我们首先要部署的策略。位于下游的安全控制，如授权(Authorization, AuthZ)、审计日志(Audit log)等，几乎都需要用户的身份。</p>
<p>身份认证的英文是Authentication，简写为AuthN，大家不要将之与授权Authorization(AuthZ)混淆(在后续系列文章中会继续探讨AuthZ相关的内容)，他们所要解决的问题相似，但有不同，也有先后。通常先AuthN，再AuthZ。我们可以用下面的比喻来形象地解释二者的联系与差异:</p>
<ul>
<li>AuthN就像是进入公司大楼的安检，负责检查员工的身份是否合法，是否具有进入公司的资格，<strong>它解决的是验证员工身份的问题</strong>。</li>
<li>AuthZ更像是公司内部的权限管理，某个员工进入了公司后(AuthN后)想访问一些重要资料，这时还需要确认该员工是否有相应的访问权限。它解决的是授权访问控制的问题。</li>
</ul>
<p>简单来说，<strong>AuthN是验证你是谁</strong>，authZ是验证你有哪些权限。AuthN解决认证问题，AuthZ解决授权问题，这两个都重要，AuthN解决外部的安全问题，authZ解决内部的安全与合规问题。</p>
<h3>1.2 身份认证的三要素</h3>
<p>身份认证需要被认证方提供一些身份信息输入，这些代表身份信息的输入被称为身份认证要素（authentication factor）。这些要素有很多，大致可分为三类：</p>
<ul>
<li>你知道的东西(What you know)</li>
</ul>
<p>即基于被认证方知道的特定信息来验证身份，最常见的如密码等。</p>
<ul>
<li>你拥有的东西(What you have)</li>
</ul>
<p>基于被认证方所拥有的特定物件来验证身份，最常见的利用数字证书、令牌卡等。N年前，在移动端应用还没有发展起来时，一些人在银行办理电子银行业务时会拿到一个U盾(又称为USBKey)，其中存放着用于用户身份识别的数字证书，这个U盾就属于此类要素。</p>
<p>上面比喻中进入大楼时使用的员工卡也属于这类要素。</p>
<ul>
<li>你本身就具有的(What you are)</li>
</ul>
<p>即基于被认证方所拥有的生物特征要素(biometric factor)来验证身份，最常见的人脸识别、指纹/声纹/虹膜识别和解锁等。理论上来说，具备个人生物特征的身份认证标志具有不可仿冒性、唯一性。</p>
<p>如果上面比喻中的大楼已经开启了人脸识别功能，那么基于人脸识别的认证就属于这类要素的认证。</p>
<p>通常我们会基于单个要素设计身份认证方案，一旦使用两个或两个以上不同类的要素，就可以被称为<strong><a href="https://wiki2.org/en/Multi-factor_authentication">双因素认证(2FA)</a></strong>或<strong>多因素认证(MFA)</strong>了。不过，2FA和MFA都比较复杂，不再本篇文章讨论范围之内。</p>
<p>基于上述要素，我们就可以设计和实现各种适合不同类别Web应用或API服务的身份认证方法了。Web应用和API服务都需要身份认证，它们有什么差异呢？这些差异是否会对身份认证方案产生影响呢？我们接下来看一下。</p>
<h3>1.3 Web应用身份认证 vs. API服务身份认证</h3>
<p>Web应用和API服务主要有以下几点区别:</p>
<ul>
<li>交互方式不同</li>
</ul>
<p>Web应用是浏览器与服务器之间的交互，用户通过浏览器访问Web应用。而API服务是程序/应用与服务器之间的交互，通过API请求获取数据或执行操作。</p>
<ul>
<li>返回数据格式不同  </li>
</ul>
<p>Web应用通常会返回html/js/css等浏览器可解析执行的代码，而API服务通常返回结构化数据，常见的如JSON或XML等。</p>
<ul>
<li>使用场景不同</li>
</ul>
<p>Web应用主要面向人类用户的使用，用户通过浏览器进行操作。而API服务主要被其他程序调用，为程序之间提供接口与数据支撑。</p>
<ul>
<li>状态管理不同</li>
</ul>
<p>Web应用在服务端保存会话状态，浏览器通过cookie等保存用户状态。而API服务通常是无状态的，每次请求都需要携带用于身份认证的信息，比如访问令牌或API Key等。</p>
<ul>
<li>安全方面的关注点不同</li>
</ul>
<p>Web应用更关注<a href="https://owasp.org/www-community/attacks/xss/">XSS</a>、<a href="https://owasp.org/www-community/attacks/csrf">CSRF</a>等输入验证安全，而API服务更关注身份认证(authN)、授权(authZ)、准入(admission)、限流等访问控制安全。</p>
<p>总之，Web应用注重界面的展示和用户交互；而API服务注重数据和服务的提供，它们有不同的使用场景、交互方式和安全关注点。</p>
<p>Web应用和API服务的这些差异也导致了Web应用和API服务适合使用的身份认证方案上会有所不同。但<strong>前后端分离架构的出现和普及</strong>，让前后端责任分离：前端专注于视图和交互，后端专注数据和业务，并且前后端通过标准化的API接口进行数据交互。这可以让后端提供统一的认证接口，不同的前端可以共享。像基于Token这样的无状态易理解的身份验证机制逐渐成为主流。也就是说，架构模式的变化，使得Web应用和API服务在身份验证(authN)方案上出现了一些融合的现象，因此在身份认证方法上，Web应用和API服务也存在一些交集。</p>
<p>下面维韦恩图列出了三类身份认证方法，包括仅适用于Web应用的、仅适用于API服务的以及两者都适用的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-3.png" alt="" /></p>
<p>本文聚焦Web应用的身份认证方式，接下来会重点说说上图中绿色背景色的几种身份认证方式。</p>
<h2>2. 安全信道是身份认证的前提和基础</h2>
<p>在对具体的Web身份认证方式进行说明之前，我们先来了解一下身份认证的前提和基础 &#8211; <strong>安全信道</strong>。</p>
<p>在Web应用身份认证的过程中，无论采用何种认证方式，用户的身份要素信息(用户名/密码、token、生物特征信息)都要传递给服务器，这时候如果传递此类信息的通信信道不安全，这些重要的认证要素信息就很容易被中间人截取、破解、篡改并被冒充，从而获得Web应用的使用权。从服务端角度来看，如果没有安全信道，服务器身份也容易被伪装，导致用户连接到“冒牌服务器”并导致严重后果。因此，没有建立在安全信道上的身份认证是不安全，不具备实际应用价值的，甚至是完全没有意义的。</p>
<p>此外，安全信道不仅对登录阶段的身份认证环节有重要意义，在用户已登录并访问Web应用其他功能页面时，安全通道也可以对数据的传输以及类似访问令牌或Cookie数据的传输起到加密和保护作用。</p>
<p>在Web应用领域，最常用的安全信道建立方式是基于HTTPS(HTTP over TLS)或直接建立在TLS之上的自定义通信，TLS利用证书对通信进行加密、验证服务器身份（甚至是客户端身份的验证），保障信息的机密性和完整性。各大安全规范和标准如<a href="https://www.pcisecuritystandards.org/">PCI DSS(Payment Card Industry Data Security Standard)</a>、<a href="https://owasp.org">OWASP</a>也强制要求使用HTTPS保障认证安全。</p>
<p>基于安全信道，我们还可以<strong>实施第一波的身份认证</strong>，这就是我们通常所说的<strong>基于HTTPS(或TLS)的双向身份认证</strong>。</p>
<blockquote>
<p>注：在我的<a href="https://item.jd.com/13694000.html">《Go语言精进之路vol2》</a>一书中，对TLS的机制以及<a href="https://tonybai.com/2015/04/30/go-and-https">基于Go标准库的TLS的双向认证</a>有系统全面的说明，欢迎各位童鞋阅读反馈。</p>
</blockquote>
<p>这种认证方式采用的是身份认证要素中的第二类要素：What you have。客户端带着归属于自己的专有证书去服务端做身份验证。如果client证书通过服务端的验签后，便可允许client进入“大楼”。</p>
<p>下面是一个基于TLS证书做身份认证的客户端与服务端交互的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-4.png" alt="" /></p>
<p>我们先看看对应上述示意图中的客户端的代码：</p>
<pre><code>// authn-examples/tls-authn/client/main.go

func main() {

    // 1. 读取客户端证书文件
    clientCert, err := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")
    if err != nil {
        log.Fatal(err)
    }

    // 2. 读取中间CA证书文件
    caCert, err := os.ReadFile("inter-cert.pem")
    if err != nil {
        log.Fatal(err)
    }
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)

    // 3. 发送请求

    client := &amp;http.Client{
        Transport: &amp;http.Transport{
            TLSClientConfig: &amp;tls.Config{
                Certificates: []tls.Certificate{clientCert},
                RootCAs:      certPool,
            },
        },
    }

    req, err := http.NewRequest("GET", "https://server.com:8443", nil)
    if err != nil {
        log.Fatal(err)
    }
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    // 4. 打印响应信息
    fmt.Println("Response Status:", resp.Status)
    //  fmt.Println("Response Headers:", resp.Header)
    body, _ := io.ReadAll(resp.Body)
    fmt.Println("Response Body:", string(body))
}
</code></pre>
<p>客户端加载client-cert.pem作为后续与服务端通信的身份凭证，加载inter-cert.pem用于校验服务端在tls握手过程发来的服务端证书(server-cert.pem)，避免连接到“冒牌站点”。通过验证后，客户端向服务端发起Get请求并输出响应的内容。</p>
<p>下面是服务端的代码：</p>
<pre><code>// authn-examples/tls-authn/server/main.go

func main() {
    var validClients = map[string]struct{}{
        "client.com": struct{}{},
    }

    // 1. 加载证书文件
    cert, err := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
    if err != nil {
        log.Fatal(err)
    }

    caCert, err := os.ReadFile("inter-cert.pem")
    if err != nil {
        log.Fatal(err)
    }
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)

    // 2. 配置TLS
    tlsConfig := &amp;tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert, // will trigger the invoke of VerifyPeerCertificate
        ClientCAs:    certPool,
    }

    // tls.Config设置
    tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        // 获取客户端证书
        cert := verifiedChains[0][0]

        // 提取CN作为客户端标识
        clientID := cert.Subject.CommonName
        fmt.Println(clientID)

        _, ok := validClients[clientID]
        if !ok {
            return errors.New("invalid client id")
        }

        return nil
    }
    // 添加处理器
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })

    // 3. 创建服务器
    srv := &amp;http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
    }

    // 4. 启动服务器
    err = srv.ListenAndServeTLS("", "")
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<blockquote>
<p>注：在你的实验环境中，需要在/etc/hosts文件中添加server.com的映射ip为127.0.0.1。</p>
</blockquote>
<p>服务端代码也不复杂，比较“套路化”：加载服务端证书和中间CA证书(用于验签client端的证书)，这里将tls.Config.ClientAuth设置为RequireAndVerifyClientCert，这会触发服务端对客户端证书的验签，同时在tlsConfig.VerifyPeerCertificate不为nil的情况下，触发对tlsConfig.VerifyPeerCertificate的函数的调用，在示例代码中，我们为tlsConfig.VerifyPeerCertificate赋值了一个匿名函数实现，在这个函数中，我们提取了客户端证书中的客户端标识CN，并查看其是否在可信任的客户端ID表中。</p>
<p>在这个示例中，这个tlsConfig.VerifyPeerCertificate执行的验证有些多余，但我们在实际代码中可以使用tlsConfig.VerifyPeerCertificate来设置黑名单，拦截那些尚未过期、但可以验签通过的客户端，实现一种<strong>客户端证书过期前的作废机制</strong>。</p>
<p>此外，上述示例中客户端、服务端以及中间CA证书的制作代码与<a href="https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side">《Go TLS服务端绑定证书的几种方式》</a>一文中的证书制作很类似，大家可以直接参考本文示例代码中的tls-authn/make-certs下面的代码，这里就不赘述了。</p>
<p>通过这种基于安全信道的身份验证方式，客户端证书可以强制认证用户，理论上不需要额外再用用户名密码。认证之后客户端在这个TLS连接上发送的所有信息都将绑定其身份。</p>
<p>不过通过颁发客户端专用证书的方式仅适合一些像网络银行之类的专有业务，大多数Web应用会与客户端间建立安全信道，但不会采用客户端证书来认证用户身份，在这样的情况下，下面要说的这些身份认证方式就可以发挥作用了。</p>
<p>我们先来看一下最传统的基于密码的认证。</p>
<h2>3. 基于密码的认证</h2>
<p>基于密码的认证属于基于第一类身份认证要素：你知道的东西(What you know)的认证方式，这类认证也是Web应用中最经典、最常见的认证方式。我们先从基于传统表单承载用户名/密码说起。</p>
<h3>3.1. 基于用户名+密码的认证(传统表单方式)</h3>
<p>这是最常见的Web应用认证方式：用户通过提交包含用户名和密码的表单(Form)，服务端Web应用进行验证。下面使用这种方式的客户端与服务单的交互示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-5.png" alt="" /></p>
<p>接下来，我们看看对应上述示意图的实现代码。我们先建立一个html文件，该文件非常简单，就是一个可输入用户名和密码的表单，点击登录按钮将表单信息发送到服务端：</p>
<pre><code>// authn-examples/password/classic/login.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;登录&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;form action="http://server.com:8080/login" method="post"&gt;

  &lt;label&gt;用户名:&lt;/label&gt;
  &lt;input type="text" name="username"/&gt;

  &lt;label&gt;密码:&lt;/label&gt;
  &lt;input type="password" name="password"/&gt;

  &lt;button type="submit"&gt;登录&lt;/button&gt;

&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>发送的HTTP Post请求的包体(Body)中会包含页面输入的username和password的值，形式如下：</p>
<pre><code>username=admin&amp;password=123456
</code></pre>
<p>而我们的服务端的代码如下：</p>
<pre><code>// authn-examples/password/classic/main.go

func main() {
    http.HandleFunc("/login", login)
    http.ListenAndServe(":8080", nil)
}

func login(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    if isValidUser(username, password) {
        w.Write([]byte("Welcome!"))
        return
    }

    http.Error(w, "Invalid username or password", http.StatusUnauthorized) // 401
}

var credentials = map[string]string{
    "admin": "123456",
}

func isValidUser(username, password string) bool {
    // 验证用户名密码
    v, ok := credentials[username]
    if !ok {
        return false
    }

    if v != password {
        return false
    }
    return true
}
</code></pre>
<p>服务端通过Request的FormValue方法获得username和password的值，并与credentials存储的合法用户信息比对(当然这只是演示代码中的临时手段，生产中不要这么存储用户信息)，比对成功，返回”Welcome”应答；比对失败，返回401 Unauthorized错误。</p>
<blockquote>
<p>注：包括本示例在内的后续所有示例的客户端和服务端都在非安全信道上通信，目的是简化示例代码的编写。大家在生产环境务必建立安全信道后再做后续的身份验证。</p>
</blockquote>
<p>基于传统的表单用户名和密码可以作为Web应用服务端身份验证的方案，但问题来了：服务端认证成功后，用户后续向Web应用服务端发起的请求是否还要继续带上用户和密码信息呢？如果不带上用户和密码信息，服务端又如何验证这些请求是来自之前已经认证成功后的用户；如果后续每个请求都带上以Form形式承载的用户名和密码，使用起来又非常不方便，还影响后续请求的正常数据的传输(对Body数据有侵入)。</p>
<p>于是便有了Session(会话)机制，它可以被认为是基于经典的用户名密码(表单承载)认证方式的“延续”，使得密码认证的成果不再局限在缺乏连续性的单一请求级别上，而是<strong>扩展到后续的一段时间内或一系列与Web应用的互操作过程中</strong>，变成了连续、持久的登录会话。</p>
<p>接下来，我们就来简单看看基于Session的后续认证方式是如何工作的。</p>
<h3>3.2 使用Session：有状态的认证方式</h3>
<p>基于Session的认证方式是一种有状态的方案，服务端会为每个身份认证成功的用户建立并保存相关session信息，同时服务端也会要求客户端在浏览器侧持久化与该Session有关少量信息，通常客户端会通过开启Cookie的方式来保存与用户Session相关的信息。</p>
<p>服务端保存Session有多种方式，可以在进程内存中、文件中、数据库、缓存(Redis)等，不同方式各有优缺点，比如将Session保存在内存中，最大的好处就是实现简单且速度快，但由于不能持久化，服务实例重启后就会丢失，此外当服务端有多副本时，session信息无法在多实例共享；使用关系数据库来保存session，可以方便持久化，也方便与服务端多实例用户数据共享，但数据库交互成本较大；而使用缓存(Redis)存储session信息是目前比较主流的方式，简单、安全、快速，还可以很好地适合分布式环境下session的共享。</p>
<p>下面是一个常见的基于cookie实现的session机制的客户端与服务端的交互示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-6.png" alt="" /></p>
<p>这里也给出上述示意图的一个参考实现示例（代码仅用作演示，很多值设置并不规范和安全，不要用于生产）。</p>
<p>session机制的开启从用户登录开始，这个示例里的login.html与上一个示例是一样的：</p>
<pre><code>// authn-examples/password/session/login.html

&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;登录&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;form action="http://server.com:8080/login" method="post"&gt;

  &lt;label&gt;用户名:&lt;/label&gt;
  &lt;input type="text" name="username"/&gt;

  &lt;label&gt;密码:&lt;/label&gt;
  &lt;input type="password" name="password"/&gt;

  &lt;button type="submit"&gt;登录&lt;/button&gt;

&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>服务端负责的login Handler代码如下：</p>
<pre><code>// authn-examples/password/session/main.go

var store = sessions.NewCookieStore([]byte("session-key"))

func main() {
    http.HandleFunc("/login", login)
    http.HandleFunc("/calc", calc)
    http.HandleFunc("/calcAdd", calcAdd)

    http.ListenAndServe(":8080", nil)
}

var credentials = map[string]string{
    "admin": "123456",
    "test":  "654321",
}

func isValid(username, password string) bool {
    // 验证用户名密码
    v, ok := credentials[username]
    if !ok {
        return false
    }

    if v != password {
        return false
    }
    return true
}

func base64Encode(src string) string {
    encoded := base64.StdEncoding.EncodeToString([]byte(src))
    return encoded
}

func base64Decode(encoded string) string {
    decoded, _ := base64.StdEncoding.DecodeString(encoded)
    return string(decoded)
}

func randomStr() string {
    // 生成随机数
    rand.Seed(time.Now().UnixNano())
    random := rand.Intn(100000)

    // 格式化为05位字符串
    str := fmt.Sprintf("%05d", random)

    return str
}

func login(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    if isValid(username, password) {
        session, err := store.Get(r, "server.com_"+username)
        if err != nil {
            fmt.Println("get session from session store error:", err)
            http.Error(w, "Internal error", http.StatusInternalServerError)
        }

        // 设置session数据
        random := randomStr()
        usernameB64 := base64Encode(username + "-" + random)
        session.Values["random"] = random
        session.Save(r, w)

        // 设置cookie
        cookie := http.Cookie{Name: "server.com-session", Value: usernameB64}
        http.SetCookie(w, &amp;cookie)

        // 登录成功,跳转到calc页面
        http.Redirect(w, r, "/calc", http.StatusSeeOther)
    } else {
        http.Error(w, "Invalid username or password", http.StatusUnauthorized) // 401
    }
}
</code></pre>
<p>我们使用了gorilla/sessions这个Go社区广泛使用的session库来实现服务端session的相关操作。以admin用户登录为例，当用户名和密码认证成功后，我们在session store中创建一个新的session：server.com_admin。然后生成一个随机数，将随机数存储在该session的名为”random”的key的下面。之后，让客户端设置cookie，name为server.com-session。值为username和random按特定格式组合后的base64编码值。</p>
<p>登录成功后，浏览器会跳到calc页面，这里我们输入两个整数，并点击”calc”按钮提交，提交动作会发送请求到calcAdd Handler中：</p>
<pre><code>// authn-examples/password/session/main.go

func calcAdd(w http.ResponseWriter, r *http.Request) {
    // 1. 获取Cookie中的Session
    cookie, err := r.Cookie("server.com-session")
    if err != nil {
        http.Error(w, "找不到cookie，请重新登录", 401)
        return
    }
    fmt.Printf("found cookie: %#v\n", cookie)

    // 2. 获取Session对象
    usernameB64 := cookie.Value
    usernameWithRandom := base64Decode(usernameB64)

    ss := strings.Split(usernameWithRandom, "-")
    username := ss[0]
    random := ss[1]
    session, err := store.Get(r, "server.com_"+username)
    if err != nil {
        http.Error(w, "找不到session, 请重新登录", 401)
        return
    }

    randomInSs := session.Values["random"]
    if random != randomInSs {
        http.Error(w, "session中信息不匹配, 请重新登录", 401)
        return
    }

    // 3. 转换为整型参数
    a, err := strconv.Atoi(r.FormValue("a"))
    if err != nil {
        http.Error(w, "参数错误", 400)
        return
    }

    b, err := strconv.Atoi(r.FormValue("b"))
    if err != nil {
        http.Error(w, "参数错误", 400)
        return
    }

    // 4. 计算并返回结果
    result := a + b
    w.Write([]byte(fmt.Sprintf("%d", result)))
}
</code></pre>
<p>calcAdd Handler会提取Cookie “server.com-session”中的值，根据值信息查找服务端本地是否存储了对应的session，并校验与session中存储的随机码是否一致。验证通过后，直接返回结算结果；否则提醒客户端重新登录。</p>
<p>前面说过，session是一种有状态的辅助身份认证机制，需要客户端和服务端的配合完成，一旦客户端禁用了Cookie机制，上述的示例实现就失效了。当然有读者会说，Session可以不基于Cookie来实现，可以用URL重写、隐藏表单字段、将Session ID放入URL路径等方式来实现，客户端也可以用LocalStorage等前端存储机制来替代Cookie。但无论哪种实现，这种有状态机制带来的复杂性都不低，并且在分布式环境中需要session共享和同步机制，影响了scaling。</p>
<p>随着微服务架构的广泛使用，无需在服务端存储额外信息、天然支持后端服务分布式多实例的无状态的连续身份认证机制受到了更多的青睐。</p>
<p>其实基于HTTP的无状态认证机制早已有之，最常见的莫过于Basic Auth了，接下来，我们就从Basic Auth开始，说几种无状态身份认证机制。</p>
<h3>3.3 Basic Auth：最早的无状态认证方式</h3>
<p>Basic Auth是HTTP最原始的身份验证方式，在HTTP1.0规范中就已存在，其原因是HTTP是无状态协议，每次请求都需要进行身份验证才能访问受保护资源。</p>
<p>Basic Auth的原理也十分简单，客户端与服务端的交互如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-7.png" alt="" /></p>
<p>Basic Auth通过在客户端的请求报文中添加HTTP Authorization Header的形式向服务器端发送认证凭据。HTTP Authorization Header的构建通常分两步。</p>
<ul>
<li>将“username:password”的组合字符串进行Base64编码，编码值记作b64Token。</li>
<li>将Authorization: Basic b64Token作为HTTP header的一个字段发送给服务器端。</li>
</ul>
<p>服务端收到请请求后提取出Authorization字段并做Base64解码，得到username和password，然后与存储的信息作比对进行客户端身份认证。</p>
<p>我们来看一个与上图对应的示例的代码，先看客户端：</p>
<pre><code>// authn-examples/password/basic/client/main.go

func main() {
    client := &amp;http.Client{}
    req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)

    // 发送默认请求
    response, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 解析响应头
    authHeader := response.Header.Get("WWW-Authenticate")
    loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
    username := "admin"
    password := "123456"

    // 判断认证类型
    if !strings.Contains(authHeader, "Basic") {
        // 不支持的认证类型
        fmt.Println("Unsupported authentication type:", authHeader)
        return
    }

    // 使用Basic Auth, 添加Basic Auth头
    loginReq.SetBasicAuth(username, password)
    response, err = client.Do(loginReq)

    // 打印响应状态
    fmt.Println(response.StatusCode)

    // 打印响应包体
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}
</code></pre>
<p>客户端的代码比较简单，并且流程与图中的交互流程是完全一样的。而服务端就是一个简单的http server，对来自客户端的带有basic auth的请求进行身份认证：</p>
<pre><code>// authn-examples/password/basic/server/main.go

func main() {
    // 创建一个基本的HTTP服务器
    mux := http.NewServeMux()

    username := "admin"
    password := "123456"

    // 针对/的handler
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        // 返回401 Unauthorized响应
        w.Header().Set("WWW-Authenticate", "Basic realm=\"server.com\"")
        w.WriteHeader(http.StatusUnauthorized)
    })

    // login handler
    mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
        // 从请求头中获取Basic Auth认证信息
        user, pass, ok := req.BasicAuth()
        if !ok {
            // 认证失败
            w.WriteHeader(http.StatusUnauthorized)
            return
        }

        // 验证用户名密码
        if user == username &amp;&amp; pass == password {
            // 认证成功
            w.WriteHeader(http.StatusOK)
            w.Write([]byte("Welcome to the protected resource!"))
        } else {
            // 认证失败
            http.Error(w, "Invalid username or password", http.StatusUnauthorized)
        }
    })

    // 监听8080端口
    err := http.ListenAndServe(":8080", mux)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>采用Basic Auth身份认证方案的客户端在每个请求中都要在Header中加上Basic Auth形式的身份信息，但服务端无需像Session那样存储任何额外的信息。</p>
<p>不过很显然，Basic Auth这种采用明文传输身份信息的方式在安全性方面饱受诟病，为了避免在Header传输明文的安全问题，RFC 2617(以及后续更新版RFC 7616)定义了HTTP Digest身份认证方式。Digest访问认证不再明文传输密码，而是传递用hash算法处理后密码摘要，相对Basic Auth验证安全性更高。接下来，我们就来看看HTTP Digest认证方式。</p>
<h3>3.4 基于HTTP Digest认证</h3>
<p>Digest是一种HTTP摘要认证，你可以把它看作是Basic Auth的改良版本，针对Base64明文发送的风险，Digest认证把用户名和密码加盐（一个被称为Nonce的随机值作为盐值）后，再通过MD5/SHA等哈希算法取摘要放到请求的Header中发送出去。Digest的认证过程如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-8.png" alt="" /></p>
<p>相对于Basic Auth，Digest Auth的一些值的生成过程还是略复杂的，这里给出一个示例性质的代码示例，可能不完全符合Digest规范，大家通过示例理解Digest的认证过程就可以了。</p>
<blockquote>
<p>注：如要使用符合RFC 7616的Digest规范（或老版RFC 2617规范)，可以找一些第三方包，比如https://github.com/abbot/go-http-auth（只满足RFC 2617）。</p>
</blockquote>
<pre><code>// authn-examples/password/digest/client/main.go

func main() {
    client := &amp;http.Client{}
    req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)

    // 发送默认请求
    response, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 解析响应头
    authHeader := response.Header.Get("WWW-Authenticate")
    loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
    username := "admin"
    password := "123456"

    // 判断认证类型
    if !strings.Contains(authHeader, "Digest") {
        // 不支持的认证类型
        fmt.Println("Unsupported authentication type:", authHeader)
        return
    }

    // 使用Digest Auth

    //随机数
    cnonce := GenNonce()

    //生成HA1
    ha1 := GetHA1(username, password, cnonce)

    //构建Authorization头
    auth := "Digest username=\"" + username + "\", nonce=\"" + cnonce + "\", algorithm=MD5, response=\"" + GetResponse(ha1, cnonce) + "\""

    loginReq.Header.Set("Authorization", auth)
    response, err = client.Do(loginReq)

    // 打印响应状态
    fmt.Println(response.StatusCode)

    // 打印响应包体
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}

// 生成随机数
func GenNonce() string {
    h := md5.New()
    io.WriteString(h, fmt.Sprint(rand.Int()))
    return hex.EncodeToString(h.Sum(nil))
}

// 根据用户名密码和随机数生成HA1
func GetHA1(username, password, cnonce string) string {
    h := md5.New()
    io.WriteString(h, username+":"+cnonce+":"+password)
    return hex.EncodeToString(h.Sum(nil))
}

// 根据HA1,随机数生成response
func GetResponse(ha1, cnonce string) string {
    h := md5.New()
    io.WriteString(h, strings.ToUpper("md5")+":"+ha1+":"+cnonce+"::"+strings.ToUpper("md5"))
    return hex.EncodeToString(h.Sum(nil))
}
</code></pre>
<p>客户端使用username、password和随机数生成摘要以及一个response码，并通过请求的头Authorization字段发给服务端。</p>
<p>服务端解析Authorization字段中的各个值，然后采用同样的算法算出一个新response，与请求中的response比对，如果一致，则认为认证成功：</p>
<pre><code>// authn-examples/password/digest/server/main.go

func main() {
    mux := http.NewServeMux()

    password := "123456"

    // 针对/的handler
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        // 返回401 Unauthorized响应
        w.Header().Set("WWW-Authenticate", "Digest realm=\"server.com\"")
        w.WriteHeader(http.StatusUnauthorized)
    })

    // login handler
    mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
        fmt.Println(req.Header)

        //验证参数
        if Verify(req, password) {
            fmt.Fprintln(w, "Verify Success!")
        } else {
            w.WriteHeader(401)
            fmt.Fprintln(w, "Verify Failed!")
        }
    })

    // 监听8080端口
    err := http.ListenAndServe(":8080", mux)
    if err != nil {
        log.Fatal(err)
    }
}

func Verify(r *http.Request, password string) bool {
    auth := r.Header.Get("Authorization")
    params := strings.Split(auth, ",")
    var username, cnonce, response string

    for _, p := range params {
        p := strings.Trim(p, " ")
        kv := strings.Split(p, "=")
        if kv[0] == "Digest username" {
            username = strings.Trim(kv[1], "\"")
        }
        if kv[0] == "nonce" {
            cnonce = strings.Trim(kv[1], "\"")
        }
        if kv[0] == "response" {
            response = strings.Trim(kv[1], "\"")
        }
    }

    if username == "" {
        return false
    }

    //根据用户名密码及随机数生成HA1
    ha1 := GetHA1(username, password, cnonce)

    //自己生成response与请求中response对比
    return response == GetResponse(ha1, cnonce)
}
</code></pre>
<p>虽然实现了无状态，安全性也高于Basic Auth，但Digest方式的用户体验依然有限：每次向服务端发送请求，客户端都要进行一次复杂计算，服务端也要再做一次相同的验算和比对。</p>
<p>那么是否有一种体验更为良好的无状态身份认证方式呢？我们接下来看看基于Token的认证方式。</p>
<h2>4. 无状态：基于Token的认证</h2>
<p>基于Token的认证方式的备受青睐得益于Web领域前后端分离架构的发展以及微服务架构的流行，在API调用和网站间需要轻量级的认证机制来传递用户信息。Token认证机制正好满足这一需求，而<a href="https://jwt.io">JWT(JSON Web Token)</a>是目前Token格式标准中使用最广的一种。</p>
<h3>4.1 JWT原理</h3>
<p>JWT由头部(Header)、载荷(Payload)和签名(Signature)三部分组成，三部分之间用圆点连接，其形式如下：</p>
<pre><code>xxxxx.yyyyy.zzzzz
</code></pre>
<p>一个真实的JWT token的例子如下面来自<a href="https://jwt.io/">jwt.io</a>站点的截图)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-9.png" alt="" /></p>
<p>JWT token的生成过程也非常清晰，下图展示了上述截图中jwt token的生成过程：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-10.png" alt="" /></p>
<p>如果你不想依赖第三方库，也可以自己实现生成token的函数，下面是一个示例：</p>
<pre><code>// authn-examples/jwt/scratch/main.go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
)

type Header struct {
    Alg string `json:"alg"`
    Typ string `json:"typ"`
}

type Claims struct {
    Sub  string `json:"sub"`
    Name string `json:"name"`
    Iat  int64  `json:"iat"`
}

// GenerateToken：不依赖第三方库的JWT生成实现
func GenerateToken(claims *Claims, key string) (string, error) {
    header, _ := json.Marshal(Header{
        Alg: "HS256",
        Typ: "JWT",
    })
    // 序列化Payload
    payload, err := json.Marshal(claims)
    if err != nil {
        return "", err
    }

    // 拼接成JWT字符串
    headerEncoded := base64.RawURLEncoding.EncodeToString(header)
    payloadEncoded := base64.RawURLEncoding.EncodeToString([]byte(payload))

    encodedToSign := headerEncoded + "." + payloadEncoded

    // 使用HMAC+SHA256签名
    hash := hmac.New(sha256.New, []byte(key))
    hash.Write([]byte(encodedToSign))
    sig := hash.Sum(nil)
    sigEncoded := base64.RawURLEncoding.EncodeToString(sig)

    var token string
    token += headerEncoded
    token += "."
    token += payloadEncoded
    token += "."
    token += sigEncoded

    return token, nil
}

func main() {
    var claims = &amp;Claims{
        Sub:  "1234567890",
        Name: "John Doe",
        Iat:  1516239022,
    }

    result, _ := GenerateToken(claims, "iamtonybai")
    fmt.Println(result)
}
</code></pre>
<p>对照着上面图示的流程，理解这个示例非常容易。当然jwt.io官方也维护了一个使用简单且灵活性更好的Go module：<a href="https://github.com/golang-jwt/jwt">golang-jwt/jwt</a>，用这个go module生成上述token的示例代码如下：</p>
<pre><code>// authn-examples/jwt/golang-jwt/main.go

import (
    "fmt"
    "time"

    jwt "github.com/golang-jwt/jwt/v5"
)

type MyCustomClaims struct {
    Sub                  string `json:"sub"`
    Name                 string `json:"name"`
    jwt.RegisteredClaims        // use its Subject and IssuedAt
}

func main() {
    mySigningKey := []byte("iamtonybai")

    // Create claims with multiple fields populated
    claims := MyCustomClaims{
        Name: "John Doe",
        Sub:  "1234567890",
        RegisteredClaims: jwt.RegisteredClaims{
            IssuedAt: jwt.NewNumericDate(time.Unix(1516239022, 0)), //  1516239022
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    ss, _ := token.SignedString(mySigningKey)
    fmt.Println(ss)

    _, err := verifyToken(ss, "iamtonybai")
    if err != nil {
        fmt.Println("invalid token:", err)
        return
    }

    fmt.Println("valid token")
}
</code></pre>
<p>这段代码中还包含了一个对jwt token验证合法性的函数verifyToken，服务端每次收到客户端请求中携带的token时，都可以使用verifyToken来验证token是否合法，下面是verifyToken的实现逻辑：</p>
<pre><code>// authn-examples/jwt/golang-jwt/main.go

// verifyToken 验证JWT函数
func verifyToken(tokenString, key string) (*jwt.Token, error) {
    // 解析Token
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte(key), nil
    })

    if err != nil {
        return nil, err
    }

    // 验证签名
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, jwt.ErrSignatureInvalid
    }

    return token, nil
}
</code></pre>
<p>服务端验证token的逻辑是先解析token，得到header、payload对应的base64UrlEncoded后的结果，然后用key重新生成签名，对比生成的签名与token携带的签名是否一致。</p>
<p>那么在Web应用中如何实现基于jwt token的身份认证呢？我们继续往下看。</p>
<h3>4.2 使用JWT token做身份认证</h3>
<p>在前面讲解Basic Auth、Digest Auth时，Basic Auth、Digest等服务端认证方式利用了HTTP Header的Authorization字段，基于JWT token的认证也是基于Authorization字段，只不过前缀从Basic、Digest换成了<strong>Bearer</strong>：</p>
<pre><code>Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTc4NjE5MzIsInVzZXJuYW1lIjoiYWRtaW4ifQ.go6NhfmYPZbtHEuJ1oULG890neo0yVdtFJwfAvHhxyE
</code></pre>
<p>基于JWT token的身份认证方式的客户端与服务端的交互流程如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/understand-go-web-authn-by-example-11.png" alt="" /></p>
<p>在这幅示意图中，客户端先用basic auth方式登录服务端，服务端验证通过后，在登录应答中写入一个jwt token作为后续客户端访问服务端其他功能的依据。客户端从登录应答的包体中解析出jwt token后，可以将该token存放在LocalStorage中，然后在后续的发向该服务端的所有请求中都带上这个jwt token。服务端对这些请求都会校验其携带的jwt token，只有验证通过的请求才能被正确处理。</p>
<p>下面来看看对应示意图的示例源码，先来看一下客户端：</p>
<pre><code>// authn-examples/jwt-authn/client/main.go

func main() {
    client := &amp;http.Client{}
    req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)

    // 发送默认请求
    response, err := client.Do(req)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 解析响应头
    authHeader := response.Header.Get("WWW-Authenticate")
    loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
    username := "admin"
    password := "123456"

    // 判断认证类型
    if !strings.Contains(authHeader, "Basic") {
        // 不支持的认证类型
        fmt.Println("Unsupported authentication type:", authHeader)
        return
    }

    // 使用Basic Auth, 添加Basic Auth头
    loginReq.SetBasicAuth(username, password)
    response, err = client.Do(loginReq)

    fmt.Println(response.StatusCode)

    // 从响应包体中获取服务端分配的jwt token
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    token := string(body)
    fmt.Println("token=", token)

    // 基于token访问服务端其他功能
    apiReq, _ := http.NewRequest("POST", "http://server.com:8080/calc", nil)
    apiReq.Header.Set("Authorization", "Bearer "+token)
    response, err = client.Do(apiReq)
    fmt.Println(response.StatusCode)
    defer response.Body.Close()
    body, err = io.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}
</code></pre>
<p>客户端的操作流程与示意图一样，先用basic auth登录server，通过验证后，拿到服务端生成的token。后续到该服务端的所有请求只需在Header中带上token即可。</p>
<p>服务端的代码如下：</p>
<pre><code>// authn-examples/jwt-authn/server/main.go

func main() {
    // 创建一个基本的HTTP服务器
    mux := http.NewServeMux()

    username := "admin"
    password := "123456"
    key := "iamtonybai"

    // 针对/的handler
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        // 返回401 Unauthorized响应
        w.Header().Set("WWW-Authenticate", "Basic realm=\"server.com\"")
        w.WriteHeader(http.StatusUnauthorized)
    })

    // login handler
    mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
        // 从请求头中获取Basic Auth认证信息
        user, pass, ok := req.BasicAuth()
        if !ok {
            // 认证失败
            w.WriteHeader(http.StatusUnauthorized)
            return
        }

        // 验证用户名密码
        if user == username &amp;&amp; pass == password {
            // 认证成功，生成token
            token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
                "username": username,
                "iat":      jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
            })
            signedToken, _ := token.SignedString([]byte(key))
            w.Write([]byte(signedToken))
        } else {
            // 认证失败
            http.Error(w, "Invalid username or password", http.StatusUnauthorized)
        }
    })

    // calc handler
    mux.HandleFunc("/calc", func(w http.ResponseWriter, req *http.Request) {
        // 读取并校验jwt token
        token := req.Header.Get("Authorization")[len("Bearer "):]
        fmt.Println(token)
        if _, err := verifyToken(token, key); err != nil {
            // 认证失败
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        w.Write([]byte("invoke calc ok"))
    })

    // 监听8080端口
    err := http.ListenAndServe(":8080", mux)
    if err != nil {
        log.Fatal(err)
    }
}
</code></pre>
<p>我们看到，除了在login handler中使用basic auth做用户密码验证外，其他功能handler(如calc)中都使用token进行身份验证。</p>
<p>与传统会话式(session)认证相比，JWT是无状态的，更适用于分布式微服务架构。与Basic auth和digest相比，jwt在使用体验上又领先一筹。凭借其无需在服务端保存会话状态、天生适合分布式架构、令牌内容可以自定义扩展等优势，现阶段，jwt已广泛应用于以下场合：</p>
<ul>
<li>前后端分离的Web应用和API认证</li>
<li>跨域单点登录(SSO)</li>
<li>微服务架构下服务间认证</li>
<li>无状态和移动应用认证</li>
</ul>
<p>不过JWT认证方式也有不足，比如：客户端要承担令牌存储成本、如果令牌泄露未及时失效可能被滥用等。</p>
<p>讲到这里，从基本的用户名密码认证，到加上密码散列的Digest认证，再到应用会话管理的Session认证，以及基于令牌的JWT认证，我们见证了认证机制的不断进步和发展。</p>
<p>这些方法主要依赖账号密码这单一要素，提供了不同程度的安全性。但是随着互联网的快速发展，开发人员也在考虑改善用户名密码这种方式的使用体验，一些一次性密码认证方式便走入了我们的生活。接下来我们就来简单说一下一次性密码验证。</p>
<h2>5. 基于一次性密码验证</h2>
<p>一次性密码（One Time Password, OTP）是一种只能使用一次的密码，它在使用后立即失效。OTP生成密码的算法基于时间，在很短的时间内(一般分钟内或更短时间内)只能使用一次；每次验证都需要生成和输入新的密码，不能重复使用。</p>
<p>一次性密码的优势主要有以下几点：</p>
<ul>
<li>安全性高：一次性密码只能使用一次，因此即使攻击者获得了密码，也无法重复使用。</li>
<li>易用性强：一次性密码通常是数字或字母组成的短语，易于记忆和输入。</li>
<li>成本低：一次性密码的生成和验证成本相对较低。</li>
</ul>
<p>信息论已经从理论上证明了：一次性密码本是无条件安全的，在理论上是无法破译的。不过现实中，还没有一种理想的一次性密码，大多数一次性密码还处于身份认证的辅助地位，多作为第二要素。</p>
<p>短信验证码就是一种我们生活中常见的一次性密码，它是利用移动运营商的短信通道传输的一次性密码。短信验证码通常由6位数字组成，有效期为几分钟，并且只能使用一次，通过短信发送给用户，非常方便用户使用，用户无需有记住密码的烦恼。</p>
<p>短信验证码的工作流程如下：</p>
<ul>
<li>客户端发起认证请求，如登录或注册；</li>
<li>服务器生成6位随机数字作为验证码，通过文本短信发送到用户注册的手机号；</li>
<li>用户接收短信并输入验证码进行验证；</li>
<li>服务器通过时间戳验证此验证码是否有效(一般在5分钟内)。</li>
<li>验证码只能使用一次，服务器会将此条记录标记为使用。</li>
</ul>
<p>短信验证码的优势是<strong>方便快捷</strong>。目前国内大多数主流Web应用都支持手机验证码登录。短信验证码通常用于以下场景：</p>
<ul>
<li>用户注册</li>
<li>用户登录</li>
<li>支付或交易</li>
<li>辅助密码找回等</li>
</ul>
<p>不过手机验证码这种一次性密码的安全性相对较低，因为短信可以被截获，攻击者可以通过截获短信来获取验证码。</p>
<p>除短信验证码外，还有其他常见的OTP实现形式:</p>
<ul>
<li>手机应用软件OTP：使用专门的手机APP软件生成OTP码，如Google Authenticator、Microsoft Authenticator等。</li>
<li>电子邮件OTP：类似短信验证码，但通过邮件发送6-8位数字验证码到用户注册的邮箱。</li>
<li>语音验证码OTP：服务端调用第三方语音平台，使用文本到语音功能给用户自动拨打认证电话，提示验证码。</li>
</ul>
<p>总体来说，OTP越来越多地被用到用户身份认证上来，随着以后技术的进步，其应用的广度和深度会进一步扩大，安全性也会得到进一步提升。基于传统密码的认证方式早晚会被扔到历史的旧物箱中。一些大厂，如Google都在研究替代传统密码的技术，比如<a href="https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/">Passkey</a>等，一些Web标准组织也在做无密码认证的规范，比如<a href="https://webauthn.guide">WebAuthn</a>等。</p>
<h2>6. 小结</h2>
<p>就写到这里吧，篇幅有些长了，关于OAuth、OpenID等身份认证技术就不在这里写了，后续找机会单独梳理。</p>
<p>本文我们介绍了多种Web应用的身份认证技术方案，各种认证技术会依据对安全性、使用性和扩展性的不同需求而存在和发展。了解每种技术的原理和优劣势，可帮助我们更好地选择适合的方案。</p>
<p>首次梳理这么多Web应用身份认证的资料，可能有些描述并不完全正确，欢迎指正。在撰写本文时，大语言模型帮助编写部分文字素材和代码。</p>
<p>本文示例所涉及的Go源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/authn-examples">这里</a>下载。</p>
<h2>7. 参考资料</h2>
<ul>
<li><a href="https://book.douban.com/subject/36039150/">《API安全实战》</a> &#8211; https://book.douban.com/subject/36039150/ </li>
<li><a href="https://book.douban.com/subject/35429043/">《API安全技术与实战》</a> &#8211; https://book.douban.com/subject/35429043/</li>
<li><a href="https://book.douban.com/subject/36179106/">《深入浅出密码学》</a> &#8211; https://book.douban.com/subject/36179106/</li>
<li><a href="https://testdriven.io/blog/web-authentication-methods">Web Authentication Methods Compared</a> &#8211; https://testdriven.io/blog/web-authentication-methods/</li>
<li><a href="https://time.geekbang.org/column/article/329954">认证：系统如何正确分辨操作用户的真实身份？</a> &#8211; https://time.geekbang.org/column/article/329954</li>
<li><a href="https://time.geekbang.org/column/article/345593">如何实现零信任网络下安全的服务访问？</a> &#8211; https://time.geekbang.org/column/article/345593</li>
<li><a href="https://time.geekbang.org/column/article/333272">凭证：系统如何保证与用户之间的承诺是准确完整且不可抵赖的？</a> &#8211; https://time.geekbang.org/column/article/333272 </li>
<li><a href="https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/">谷歌正推出Passkey，密码将成历史</a> &#8211; https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/</li>
<li><a href="https://www.microsoft.com/zh-cn/security/business/security-101/what-is-authentication">What is authentication?</a> &#8211; https://www.microsoft.com/zh-cn/security/business/security-101/what-is-authentication</li>
<li><a href="https://en.wikipedia.org/wiki/Authentication.html">Authentication(wikipedia)</a> &#8211; https://en.wikipedia.org/wiki/Authentication.html</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7617">RFC 7617: The &#8216;Basic&#8217; HTTP Authentication Scheme</a> &#8211; https://datatracker.ietf.org/doc/html/rfc7617</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7616">RFC 7616: HTTP Digest Access Authentication</a> &#8211; https://datatracker.ietf.org/doc/html/rfc7616</li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519: JSON Web Token(JWT)</a> &#8211; https://datatracker.ietf.org/doc/html/rfc7519</li>
<li><a href="https://jwt.io/introduction">Introduction to JSON Web Tokens</a> &#8211; https://jwt.io/introduction</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Gopher Daily改版了</title>
		<link>https://tonybai.com/2023/08/06/gopherdaily-revamped/</link>
		<comments>https://tonybai.com/2023/08/06/gopherdaily-revamped/#comments</comments>
		<pubDate>Sun, 06 Aug 2023 12:24:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[atom]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[caddy]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[feed]]></category>
		<category><![CDATA[filepath]]></category>
		<category><![CDATA[footer]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[github-pages]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[gofeed]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopherdaily]]></category>
		<category><![CDATA[Gopher部落]]></category>
		<category><![CDATA[header]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[Mail]]></category>
		<category><![CDATA[Markdown]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[NLP]]></category>
		<category><![CDATA[regexp]]></category>
		<category><![CDATA[rss]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[ZeroSSL]]></category>
		<category><![CDATA[大语言模型]]></category>
		<category><![CDATA[摘要]]></category>
		<category><![CDATA[正则表达式]]></category>
		<category><![CDATA[知识星球]]></category>
		<category><![CDATA[翻译]]></category>
		<category><![CDATA[自动化]]></category>
		<category><![CDATA[自然语言处理]]></category>
		<category><![CDATA[订阅]]></category>
		<category><![CDATA[邮件列表]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3956</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/08/06/gopherdaily-revamped 已经记不得GopherDaily是何时创建的了，翻了一下GopherDaily项目的commit history，才发现我的这个个人项目是2019年9月创建的，最初内容组织很粗糙，但我的编辑制作的热情很高，基本能坚持每日一发，甚至节假日也不停刊： 该项目的初衷就是为广大Gopher带来新鲜度较高的Go语言技术资料。项目创建以来得到了很多Gopher的支持，甚至经常收到催刊邮件/私信以及主动report订阅列表问题的情况。 不过近一年多，订阅GopherDaily的Gopher可能会发现：GopherDaily已经做不到“Daily”了！究其原因还是个人精力有限，每刊编辑都要花费很多时间。但个人又不想暂停该项目，怎么办呢？近段时间我就在着手思考提升GopherDaily制作效率的问题。 一个可行的方案就是“半自动化”！在这次从“纯人工”到“半自动化”的过程中，顺便对GopherDaily做了一次“改版”。 在这篇文章中，我就来说说结合大语言模型和Go技术栈实现GopherDaily制作的“半自动化”以及GopherDaily“改版”的历程。 1. “半自动化”的制作流程 当前的GopherDaily每刊的制作过程十分费时费力，下面是图示的制作过程： 这里面所有步骤都是人工处理，且收集资料、阅读摘要以及选优最为耗时。 那么这些环节中哪些可以自动化呢？收集、摘要、翻译、生成与发布都可以自动化，只有“选优”需要人工干预，下面是改进后的“半自动化”流程： 我们看到整个过程分为三个阶段： 第一阶段(stage1)：自动化的收集资料，并生成第二阶段的输入issue-20230805-stage1.json(以2023年8月5日为例)。 第二阶段(stage2)：对输入的issue-20230805-stage1.json中的资料进行选优，删掉不适合或质量不高的资料，当然也可以手工加入一些自动化收集阶段未找到的优秀资料；然后基于选优后的内容生成issue-20230805-stage2.json，作为第三阶段的输入。 第三阶段(stage3)：这一阶段也都是自动化的，程序基于第二阶段的输出issue-20230805-stage2.json中内容，逐条生成摘要，并将文章标题和摘要翻译为中文，最后生成两个文件：issue-20230805.html和issue-20230805.md，前者将被发布到邮件列表和gopherdaily github page上，而后者则会被上传到传统的GopherDaily归档项目中。 我个人的目标是将改进后的整个“半自动化”过程缩短在半小时以内，从试运行效果来看，基本达成！ 下面我就来简要聊聊各个自动化步骤是如何实现的。 2. Go技术资料自动收集 GopherDaily制作效率提升的一个大前提就是可以将最耗时的“资料收集”环节自动化了！而要做到这一点，下面两方面不可或缺： 资料源集合 针对资料源的最新文章的感知和拉取 2.1 资料源的来源 资料源从哪里来呢？答案是以往的GopherDaily issues中！四年来积累了数千篇文章的URL，从这些issue中提取URL并按URL中域名/域名+一级路径的出现次数做个排序，得到GopherDaily改版后的初始资料源集合。虽然这个方案并不完美，但至少可以满足改版后的初始需求，后续还可以对资料源做渐进的手工优化。 提取文本中URL的方法有很多种，常用的一种方法是使用正则表达式，下面是一个从markdown或txt文件中提取url并输出的例子： // extract-url/main.go package main import ( "bufio" "fmt" "os" "path/filepath" "regexp" ) func main() { var allURLs []string err := filepath.Walk("/Users/tonybai/blog/gitee.com/gopherdaily", func(path string, [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/gopherdaily-revamped-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/08/06/gopherdaily-revamped">本文永久链接</a> &#8211; https://tonybai.com/2023/08/06/gopherdaily-revamped</p>
<p>已经记不得GopherDaily是何时创建的了，翻了一下<a href="https://github.com/bigwhite/gopherdaily">GopherDaily项目</a>的commit history，才发现我的这个个人项目是2019年9月创建的，<a href="https://github.com/bigwhite/gopherdaily/blob/master/201909/issue-20190925.txt">最初内容组织很粗糙</a>，但我的编辑制作的热情很高，基本能坚持<strong>每日一发</strong>，甚至节假日也<strong>不停刊</strong>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/gopherdaily-revamped-2.png" alt="" /></p>
<p>该项目的初衷就是<strong>为广大Gopher带来新鲜度较高的Go语言技术资料</strong>。项目创建以来得到了很多Gopher的支持，甚至经常收到催刊邮件/私信以及主动report订阅列表问题的情况。</p>
<p>不过近一年多，订阅GopherDaily的Gopher可能会发现：GopherDaily已经做不到“Daily”了！究其原因还是个人精力有限，每刊编辑都要花费很多时间。但个人又不想暂停该项目，怎么办呢？近段时间我就在着手思考<strong>提升GopherDaily制作效率的问题</strong>。</p>
<p>一个可行的方案就是“半自动化”！在这次从“纯人工”到“半自动化”的过程中，顺便对GopherDaily做了一次“改版”。</p>
<p>在这篇文章中，我就来说说结合<a href="https://en.wikipedia.org/wiki/Large_language_model">大语言模型</a>和Go技术栈实现GopherDaily制作的“半自动化”以及GopherDaily“改版”的历程。</p>
<h2>1. “半自动化”的制作流程</h2>
<p>当前的GopherDaily每刊的制作过程十分费时费力，下面是图示的制作过程：</p>
<p><img src="https://tonybai.com/wp-content/uploads/gopherdaily-revamped-3.png" alt="" /></p>
<p>这里面所有步骤都是人工处理，且收集资料、阅读摘要以及选优最为耗时。</p>
<p>那么这些环节中哪些可以自动化呢？收集、摘要、翻译、生成与发布都可以自动化，只有“选优”需要人工干预，下面是改进后的“半自动化”流程：</p>
<p><img src="https://tonybai.com/wp-content/uploads/gopherdaily-revamped-4.png" alt="" /></p>
<p>我们看到整个过程分为三个阶段：</p>
<ul>
<li>第一阶段(stage1)：自动化的收集资料，并生成第二阶段的输入issue-20230805-stage1.json(以2023年8月5日为例)。</li>
<li>第二阶段(stage2)：对输入的issue-20230805-stage1.json中的资料进行选优，删掉不适合或质量不高的资料，当然也可以手工加入一些自动化收集阶段未找到的优秀资料；然后基于选优后的内容生成issue-20230805-stage2.json，作为第三阶段的输入。</li>
<li>第三阶段(stage3)：这一阶段也都是自动化的，程序基于第二阶段的输出issue-20230805-stage2.json中内容，逐条生成摘要，并将文章标题和摘要翻译为中文，最后生成两个文件：issue-20230805.html和issue-20230805.md，前者将被发布到<a href="https://gopherdaily.tonybai.com/subscribe">邮件列表</a>和<a href="https://gopherdaily.tonybai.com">gopherdaily github page</a>上，而后者则会被上传到<a href="https://github.com/bigwhite/gopherdaily">传统的GopherDaily归档项目</a>中。</li>
</ul>
<p>我个人的目标是将改进后的整个“半自动化”过程缩短在半小时以内，从试运行效果来看，基本达成！</p>
<p>下面我就来简要聊聊各个自动化步骤是如何实现的。</p>
<h2>2. Go技术资料自动收集</h2>
<p>GopherDaily制作效率提升的一个大前提就是可以将最耗时的“资料收集”环节自动化了！而要做到这一点，下面两方面不可或缺：</p>
<ul>
<li>资料源集合</li>
<li>针对资料源的最新文章的感知和拉取</li>
</ul>
<h3>2.1 资料源的来源</h3>
<p>资料源从哪里来呢？答案是以往的GopherDaily issues中！四年来积累了数千篇文章的URL，从这些issue中提取URL并按URL中域名/域名+一级路径的出现次数做个排序，得到GopherDaily改版后的初始资料源集合。虽然这个方案并不完美，但至少可以满足改版后的初始需求，后续还可以对资料源做渐进的手工优化。</p>
<p>提取文本中URL的方法有很多种，常用的一种方法是使用正则表达式，下面是一个从markdown或txt文件中提取url并输出的例子：</p>
<pre><code>// extract-url/main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "path/filepath"
    "regexp"
)

func main() {
    var allURLs []string

    err := filepath.Walk("/Users/tonybai/blog/gitee.com/gopherdaily", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if info.IsDir() {
            return nil
        }

        if filepath.Ext(path) != ".txt" &amp;&amp; filepath.Ext(path) != ".md" {
            return nil
        }

        file, err := os.Open(path)
        if err != nil {
            return err
        }
        defer file.Close()

        scanner := bufio.NewScanner(file)
        urlRegex := regexp.MustCompile(`https?://[^\s]+`)

        for scanner.Scan() {
            urls := urlRegex.FindAllString(scanner.Text(), -1)
            allURLs = append(allURLs, urls...)
        }

        return scanner.Err()
    })

    if err != nil {
        fmt.Println(err)
        return
    }

    for _, url := range allURLs {
        fmt.Printf("%s\n", url)
    }
    fmt.Println(len(allURLs))
}
</code></pre>
<p>我将提取并分析后得到的URL放入一个临时文件中，因为仅提取URL还不够，要做为资料源，我们需要的是对应站点的feed地址。那么如何提取出站点的feed地址呢？我们看下面这个例子：</p>
<pre><code>// extract_rss/main.go

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
)

var (
    rss  = regexp.MustCompile(`&lt;link[^&gt;]*type="application/rss\+xml"[^&gt;]*href="([^"]+)"`)
    atom = regexp.MustCompile(`&lt;link[^&gt;]*type="application/atom\+xml"[^&gt;]*href="([^"]+)"`)
)

func main() {
    var sites = []string{
        "http://research.swtch.com",
        "https://tonybai.com",
        "https://benhoyt.com/writings",
    }

    for _, url := range sites {
        resp, err := http.Get(url)
        if err != nil {
            fmt.Println("Error fetching URL:", err)
            continue
        }
        defer resp.Body.Close()

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error reading response body:", err)
            continue
        }

        matches := rss.FindAllStringSubmatch(string(body), -1)
        if len(matches) == 0 {
            matches = atom.FindAllStringSubmatch(string(body), -1)
            if len(matches) == 0 {
                continue
            }
        }

        fmt.Printf("\"%s\" -&gt; rss: \"%s\"\n", url, matches[0][1])
    }
}
</code></pre>
<p>执行上述程序，我们得到如下结果：</p>
<pre><code>"http://research.swtch.com" -&gt; rss: "http://research.swtch.com/feed.atom"
"https://tonybai.com" -&gt; rss: "https://tonybai.com/feed/"
"https://benhoyt.com/writings" -&gt; rss: "/writings/rss.xml"
</code></pre>
<p>我们看到不同站点的rss地址值着实不同，有些是完整的url地址，有些则是相对于主站点url的路径，这个还需要进一步判断与处理，但这里就不赘述了。</p>
<p>我们将提取和处理后的feed地址放入feeds.toml中作为资料源集合。每天开始制作Gopher Daily时，就从读取这个文件中的资料源开始。</p>
<h3>2.2 感知和拉取资料源的更新</h3>
<p>有了资料源集合后，我们接下来要做的就是定期感知和拉取资料源的最新更新（暂定24小时以内的），再说白点就是拉取资料源的feed数据，解析内容，得到资料源的最新文章信息。针对feed拉取与解析，Go社区有现成的工具，比如<a href="https://github.com/mmcdole/gofeed">gofeed</a>就是其中功能较为齐全且表现稳定的一个。</p>
<p>下面是使用Gofeed抓取feed地址并获取文章信息的例子：</p>
<pre><code>// gofeed/main.go

package main

import (
    "fmt"

    "github.com/mmcdole/gofeed"
)

func main() {

    var feeds = []string{
        "https://research.swtch.com/feed.atom",
        "https://tonybai.com/feed/",
        "https://benhoyt.com/writings/rss.xml",
    }

    fp := gofeed.NewParser()
    for _, feed := range feeds {
        feedInfo, err := fp.ParseURL(feed)
        if err != nil {
            fmt.Printf("parse feed [%s] error: %s\n", feed, err.Error())
            continue
        }
        fmt.Printf("The info of feed url: %s\n", feed)
        for _, item := range feedInfo.Items {
            fmt.Printf("\t title: %s\n", item.Title)
            fmt.Printf("\t link: %s\n", item.Link)
            fmt.Printf("\t published: %s\n", item.Published)
        }
        fmt.Println("")
    }
}
</code></pre>
<p>该程序分别解析三个feed地址，并分别输出得到的文章信息，包括标题、url和发布时间。运行上述程序我们将得到如下结果：</p>
<pre><code>$go run main.go
The info of feed url: https://research.swtch.com/feed.atom
     title: Coroutines for Go
     link: http://research.swtch.com/coro
     published: 2023-07-17T14:00:00-04:00
     title: Storing Data in Control Flow
     link: http://research.swtch.com/pcdata
     published: 2023-07-11T14:00:00-04:00
     title: Opting In to Transparent Telemetry
     link: http://research.swtch.com/telemetry-opt-in
     published: 2023-02-24T08:59:00-05:00
     title: Use Cases for Transparent Telemetry
     link: http://research.swtch.com/telemetry-uses
     published: 2023-02-08T08:00:03-05:00
     title: The Design of Transparent Telemetry
     link: http://research.swtch.com/telemetry-design
     published: 2023-02-08T08:00:02-05:00
     title: Transparent Telemetry for Open-Source Projects
     link: http://research.swtch.com/telemetry-intro
     published: 2023-02-08T08:00:01-05:00
     title: Transparent Telemetry
     link: http://research.swtch.com/telemetry
     published: 2023-02-08T08:00:00-05:00
     title: The Magic of Sampling, and its Limitations
     link: http://research.swtch.com/sample
     published: 2023-02-04T12:00:00-05:00
     title: Go’s Version Control History
     link: http://research.swtch.com/govcs
     published: 2022-02-14T10:00:00-05:00
     title: What NPM Should Do Today To Stop A New Colors Attack Tomorrow
     link: http://research.swtch.com/npm-colors
     published: 2022-01-10T11:45:00-05:00
     title: Our Software Dependency Problem
     link: http://research.swtch.com/deps
     published: 2019-01-23T11:00:00-05:00
     title: What is Software Engineering?
     link: http://research.swtch.com/vgo-eng
     published: 2018-05-30T10:00:00-04:00
     title: Go and Dogma
     link: http://research.swtch.com/dogma
     published: 2017-01-09T09:00:00-05:00
     title: A Tour of Acme
     link: http://research.swtch.com/acme
     published: 2012-09-17T11:00:00-04:00
     title: Minimal Boolean Formulas
     link: http://research.swtch.com/boolean
     published: 2011-05-18T00:00:00-04:00
     title: Zip Files All The Way Down
     link: http://research.swtch.com/zip
     published: 2010-03-18T00:00:00-04:00
     title: UTF-8: Bits, Bytes, and Benefits
     link: http://research.swtch.com/utf8
     published: 2010-03-05T00:00:00-05:00
     title: Computing History at Bell Labs
     link: http://research.swtch.com/bell-labs
     published: 2008-04-09T00:00:00-04:00
     title: Using Uninitialized Memory for Fun and Profit
     link: http://research.swtch.com/sparse
     published: 2008-03-14T00:00:00-04:00
     title: Play Tic-Tac-Toe with Knuth
     link: http://research.swtch.com/tictactoe
     published: 2008-01-25T00:00:00-05:00
     title: Crabs, the bitmap terror!
     link: http://research.swtch.com/crabs
     published: 2008-01-09T00:00:00-05:00

The info of feed url: https://tonybai.com/feed/
     title: Go语言开发者的Apache Arrow使用指南：读写Parquet文件
     link: https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/
     published: Mon, 31 Jul 2023 13:07:28 +0000
     title: Go语言开发者的Apache Arrow使用指南：扩展compute包
     link: https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/
     published: Sat, 22 Jul 2023 13:58:57 +0000
     title: 使用testify包辅助Go测试指南
     link: https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/
     published: Sun, 16 Jul 2023 07:09:56 +0000
     title: Go语言开发者的Apache Arrow使用指南：数据操作
     link: https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/
     published: Thu, 13 Jul 2023 14:41:25 +0000
     title: Go语言开发者的Apache Arrow使用指南：高级数据结构
     link: https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/
     published: Sat, 08 Jul 2023 15:27:54 +0000
     title: Apache Arrow：驱动列式分析性能和连接性的提升[译]
     link: https://tonybai.com/2023/07/01/arrow-columnar-analytics/
     published: Sat, 01 Jul 2023 14:42:29 +0000
     title: Go语言开发者的Apache Arrow使用指南：内存管理
     link: https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/
     published: Fri, 30 Jun 2023 14:00:59 +0000
     title: Go语言开发者的Apache Arrow使用指南：数据类型
     link: https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/
     published: Sat, 24 Jun 2023 20:43:38 +0000
     title: Go语言包设计指南
     link: https://tonybai.com/2023/06/18/go-package-design-guide/
     published: Sun, 18 Jun 2023 15:03:41 +0000
     title: Go GC：了解便利背后的开销
     link: https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/
     published: Tue, 13 Jun 2023 14:00:16 +0000

The info of feed url: https://benhoyt.com/writings/rss.xml
     title: The proposal to enhance Go's HTTP router
     link: https://benhoyt.com/writings/go-servemux-enhancements/
     published: Mon, 31 Jul 2023 08:00:00 +1200
     title: Scripting with Go: a 400-line Git client that can create a repo and push itself to GitHub
     link: https://benhoyt.com/writings/gogit/
     published: Sat, 29 Jul 2023 16:30:00 +1200
     title: Names should be as short as possible while still being clear
     link: https://benhoyt.com/writings/short-names/
     published: Mon, 03 Jul 2023 21:00:00 +1200
     title: Lookup Tables (Forth Dimensions XIX.3)
     link: https://benhoyt.com/writings/forth-lookup-tables/
     published: Sat, 01 Jul 2023 22:10:00 +1200
     title: For Python packages, file structure != API
     link: https://benhoyt.com/writings/python-api-file-structure/
     published: Fri, 30 Jun 2023 22:50:00 +1200
     title: Designing Pythonic library APIs
     link: https://benhoyt.com/writings/python-api-design/
     published: Sun, 18 Jun 2023 21:00:00 +1200
     title: From Go on EC2 to Fly.io: +fun, −$9/mo
     link: https://benhoyt.com/writings/flyio/
     published: Mon, 27 Feb 2023 10:00:00 +1300
     title: Code coverage for your AWK programs
     link: https://benhoyt.com/writings/goawk-coverage/
     published: Sat, 10 Dec 2022 13:41:00 +1300
     title: I/O is no longer the bottleneck
     link: https://benhoyt.com/writings/io-is-no-longer-the-bottleneck/
     published: Sat, 26 Nov 2022 22:20:00 +1300
     title: microPledge: our startup that (we wish) competed with Kickstarter
     link: https://benhoyt.com/writings/micropledge/
     published: Mon, 14 Nov 2022 20:00:00 +1200
     title: Rob Pike's simple C regex matcher in Go
     link: https://benhoyt.com/writings/rob-pike-regex/
     published: Fri, 12 Aug 2022 14:00:00 +1200
     title: Tools I use to build my website
     link: https://benhoyt.com/writings/tools-i-use-to-build-my-website/
     published: Tue, 02 Aug 2022 19:00:00 +1200
     title: Modernizing AWK, a 45-year old language, by adding CSV support
     link: https://benhoyt.com/writings/goawk-csv/
     published: Tue, 10 May 2022 09:30:00 +1200
     title: Prig: like AWK, but uses Go for "scripting"
     link: https://benhoyt.com/writings/prig/
     published: Sun, 27 Feb 2022 18:20:00 +0100
     title: Go performance from version 1.2 to 1.18
     link: https://benhoyt.com/writings/go-version-performance/
     published: Fri, 4 Feb 2022 09:30:00 +1300
     title: Optimizing GoAWK with a bytecode compiler and virtual machine
     link: https://benhoyt.com/writings/goawk-compiler-vm/
     published: Thu, 3 Feb 2022 22:25:00 +1300
     title: AWKGo, an AWK-to-Go compiler
     link: https://benhoyt.com/writings/awkgo/
     published: Mon, 22 Nov 2021 00:10:00 +1300
     title: Improving the code from the official Go RESTful API tutorial
     link: https://benhoyt.com/writings/web-service-stdlib/
     published: Wed, 17 Nov 2021 07:00:00 +1300
     title: Simple Lists: a tiny to-do list app written the old-school way (server-side Go, no JS)
     link: https://benhoyt.com/writings/simple-lists/
     published: Mon, 4 Oct 2021 07:30:00 +1300
     title: Structural pattern matching in Python 3.10
     link: https://benhoyt.com/writings/python-pattern-matching/
     published: Mon, 20 Sep 2021 19:30:00 +1200
     title: Mugo, a toy compiler for a subset of Go that can compile itself
     link: https://benhoyt.com/writings/mugo/
     published: Mon, 12 Apr 2021 20:30:00 +1300
     title: How to implement a hash table (in C)
     link: https://benhoyt.com/writings/hash-table-in-c/
     published: Fri, 26 Mar 2021 20:30:00 +1300
     title: Performance comparison: counting words in Python, Go, C++, C, AWK, Forth, and Rust
     link: https://benhoyt.com/writings/count-words/
     published: Mon, 15 Mar 2021 20:30:00 +1300
     title: The small web is beautiful
     link: https://benhoyt.com/writings/the-small-web-is-beautiful/
     published: Tue, 2 Mar 2021 06:50:00 +1300
     title: Coming in Go 1.16: ReadDir and DirEntry
     link: https://benhoyt.com/writings/go-readdir/
     published: Fri, 29 Jan 2021 10:00:00 +1300
     title: Fuzzing in Go
     link: https://lwn.net/Articles/829242/
     published: Tue, 25 Aug 2020 08:00:00 +1200
     title: Searching code with Sourcegraph
     link: https://lwn.net/Articles/828748/
     published: Mon, 17 Aug 2020 08:00:00 +1200
     title: Different approaches to HTTP routing in Go
     link: https://benhoyt.com/writings/go-routing/
     published: Fri, 31 Jul 2020 08:00:00 +1200
     title: Go filesystems and file embedding
     link: https://lwn.net/Articles/827215/
     published: Fri, 31 Jul 2020 00:00:00 +1200
     title: The sad, slow-motion death of Do Not Track
     link: https://lwn.net/Articles/826575/
     published: Wed, 22 Jul 2020 11:00:00 +1200
     title: What's new in Lua 5.4
     link: https://lwn.net/Articles/826134/
     published: Wed, 15 Jul 2020 11:00:00 +1200
     title: Hugo: a static-site generator
     link: https://lwn.net/Articles/825507/
     published: Wed, 8 Jul 2020 11:00:00 +1200
     title: Generics for Go
     link: https://lwn.net/Articles/824716/
     published: Wed, 1 Jul 2020 11:00:00 +1200
     title: More alternatives to Google Analytics
     link: https://lwn.net/Articles/824294/
     published: Wed, 24 Jun 2020 11:00:00 +1200
     title: Lightweight Google Analytics alternatives
     link: https://lwn.net/Articles/822568/
     published: Wed, 17 Jun 2020 11:00:00 +1200
     title: An intro to Go for non-Go developers
     link: https://benhoyt.com/writings/go-intro/
     published: Wed, 10 Jun 2020 23:38:00 +1200
     title: ZZT in Go (using a Pascal-to-Go converter)
     link: https://benhoyt.com/writings/zzt-in-go/
     published: Fri, 29 May 2020 17:25:00 +1200
     title: Testing in Go: philosophy and tools
     link: https://lwn.net/Articles/821358/
     published: Wed, 27 May 2020 12:00:00 +1200
     title: The state of the AWK
     link: https://lwn.net/Articles/820829/
     published: Wed, 20 May 2020 12:00:00 +1200
     title: What's coming in Go 1.15
     link: https://lwn.net/Articles/820217/
     published: Wed, 13 May 2020 12:00:00 +1200
     title: Don't try to sanitize input. Escape output.
     link: https://benhoyt.com/writings/dont-sanitize-do-escape/
     published: Thu, 27 Feb 2020 19:27:00 +1200
     title: SEO for Software Engineers
     link: https://benhoyt.com/writings/seo-for-software-engineers/
     published: Thu, 20 Feb 2020 12:00:00 +1200
</code></pre>
<blockquote>
<p>注：gofeed抓取的item.Description是文章的摘要。但这个摘要不一定可以真实反映文章内容的概要，很多就是文章内容的前N个字而已。</p>
</blockquote>
<p>Gopher Daily半自动化改造的另外一个技术课题是对拉取的文章做自动摘要与标题摘要的翻译，下面我们继续来看一下这个课题如何攻破。</p>
<blockquote>
<p>注：目前微信公众号的优质文章尚未实现自动拉取，还需手工选优。</p>
</blockquote>
<h2>3. 自动摘要与翻译</h2>
<p>对一段文本提取摘要和翻译均属于自然语言处理(NLP)范畴，说实话，Go在这个范畴中并不活跃，很难找到像样的开源算法实现或工具可直接使用。我的解决方案是<strong>借助云平台供应商的NLP API来做</strong>，这里我用的是微软Azure的相关API。</p>
<p>在使用现成的API之前，我们需要抓取特定url上的html页面并提取出要进行摘要的文本。</p>
<h3>3.1 提取html中的原始文本</h3>
<p>我们通过http.Get可以获取到一个文章URL上的html页面的所有内容，但如何提取出主要文本以供后续提取摘要使用呢？每个站点上的html内容都包含了很多额外内容，比如header、footer、分栏、边栏、导航栏等，这些内容对摘要的生成具有一定影响。我们最好能将这些额外内容剔除掉。但html的解析还是十分复杂的，我的解决方案是将html转换为markdown后再提交给摘要API。</p>
<p><a href="https://github.com/JohannesKaufmann/html-to-markdown">html-to-markdown</a>是一款不错的转换工具，它最吸引我的是可以删除原HTML中的一些tag，并自定义一些rule。下面的例子就是用html-to-markdown获取文章原始本文的例子：</p>
<pre><code>// get-original-text/main.go

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"

    md "github.com/JohannesKaufmann/html-to-markdown"
)

func main() {
    s, err := getOriginText("http://research.swtch.com/coro")
    if err != nil {
        panic(err)
    }
    fmt.Println(s)
}

func getOriginText(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)

    converter := md.NewConverter("", true, nil).Remove("header",
        "footer", "aside", "table", "nav") //"table" is used to store code

    markdown, err := converter.ConvertString(string(body))
    if err != nil {
        return "", err
    }
    return markdown, nil
}
</code></pre>
<p>在这个例子中，我们删除了header、footer、边栏、导航栏等，尽可能的保留主要文本。针对这个例子我就不执行了，大家可以自行执行并查看执行结果。</p>
<h3>3.2 提取摘要</h3>
<p>我们通过<a href="https://learn.microsoft.com/zh-cn/azure/ai-services/language-service/summarization/how-to/document-summarization">微软Azure提供的摘要提取API</a>进行摘要提取。微软Azure的这个API提供的免费额度，足够我这边制作Gopher Daily使用了。</p>
<blockquote>
<p>注：要使用微软Azure提供的各类免费API，需要先注册Azure的账户。目前摘要提取API仅在North Europe, East US, UK South三个region提供，创建API服务时别选错Region了。我这里用的是East US。</p>
<p>注：Azure控制台较为难用，大家要有心理准备:)。</p>
</blockquote>
<p>微软这个摘要API十分复杂，下面给出一个用curl调用API的示例。</p>
<p>摘要提取API的使用分为两步。第一步是请求对原始文本进行摘要处理，比如：</p>
<pre><code>$curl -i -X POST https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-10-01-preview \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: your_api_key" \
-d \
'
{
  "displayName": "Document Abstractive Summarization Task Example",
  "analysisInput": {
    "documents": [
      {
        "id": "1",
        "language": "en",
        "text": "At Microsoft, we have been on a quest to advance AI beyond existing techniques, by taking a more holistic, human-centric approach to learning and understanding. As Chief Technology Officer of Azure AI services, I have been working with a team of amazing scientists and engineers to turn this quest into a reality. In my role, I enjoy a unique perspective in viewing the relationship among three attributes of human cognition: monolingual text (X), audio or visual sensory signals, (Y) and multilingual (Z). At the intersection of all three, there’s magic—what we call XYZ-code as illustrated in Figure 1—a joint representation to create more powerful AI that can speak, hear, see, and understand humans better. We believe XYZ-code will enable us to fulfill our long-term vision: cross-domain transfer learning, spanning modalities and languages. The goal is to have pre-trained models that can jointly learn representations to support a broad range of downstream AI tasks, much in the way humans do today. Over the past five years, we have achieved human performance on benchmarks in conversational speech recognition, machine translation, conversational question answering, machine reading comprehension, and image captioning. These five breakthroughs provided us with strong signals toward our more ambitious aspiration to produce a leap in AI capabilities, achieving multi-sensory and multilingual learning that is closer in line with how humans learn and understand. I believe the joint XYZ-code is a foundational component of this aspiration, if grounded with external knowledge sources in the downstream AI tasks."
      }
    ]
  },
  "tasks": [
    {
      "kind": "AbstractiveSummarization",
      "taskName": "Document Abstractive Summarization Task 1",
      "parameters": {
        "sentenceCount": 1
      }
    }
  ]
}
'
</code></pre>
<p>请求成功后，我们将得到一段应答，应答中包含类似operation-location的一段地址：</p>
<pre><code>Operation-Location:[https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs/66e7e3a1-697c-4fad-864c-d84c647682b4?api-version=2022-10-01-preview]
</code></pre>
<p>这段地址就是第二步的请求地址，第二步是从这个地址获取摘要后的本文：</p>
<pre><code>$curl -X GET https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs/66e7e3a1-697c-4fad-864c-d84c647682b4\?api-version\=2022-10-01-preview \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: your_api_key"
{"jobId":"66e7e3a1-697c-4fad-864c-d84c647682b4","lastUpdatedDateTime":"2023-07-27T11:09:45Z","createdDateTime":"2023-07-27T11:09:44Z","expirationDateTime":"2023-07-28T11:09:44Z","status":"succeeded","errors":[],"displayName":"Document Abstractive Summarization Task Example","tasks":{"completed":1,"failed":0,"inProgress":0,"total":1,"items":[{"kind":"AbstractiveSummarizationLROResults","taskName":"Document Abstractive Summarization Task 1","lastUpdateDateTime":"2023-07-27T11:09:45.8892126Z","status":"succeeded","results":{"documents":[{"summaries":[{"text":"Microsoft has been working to advance AI beyond existing techniques by taking a more holistic, human-centric approach to learning and understanding, and the Chief Technology Officer of Azure AI services, who enjoys a unique perspective in viewing the relationship among three attributes of human cognition: monolingual text, audio or visual sensory signals, and multilingual, has created XYZ-code, a joint representation to create more powerful AI that can speak, hear, see, and understand humans better.","contexts":[{"offset":0,"length":1619}]}],"id":"1","warnings":[]}],"errors":[],"modelVersion":"latest"}}]}}%
</code></pre>
<p>大家可以根据请求和应答的JSON结构，结合一些json-to-struct工具自行实现Azure摘要API的Go代码。</p>
<h3>3.3 翻译</h3>
<p><a href="https://learn.microsoft.com/zh-cn/azure/ai-services/translator/reference/v3-0-reference">Azure的翻译API</a>相对于摘要API要简单的多。</p>
<p>下面是使用curl演示翻译API的示例：</p>
<pre><code>$curl -X POST "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&amp;to=zh" \
     -H "Ocp-Apim-Subscription-Key:your_api_key" \
     -H "Ocp-Apim-Subscription-Region:westcentralus" \
     -H "Content-Type: application/json" \
     -d "[{'Text':'Hello, what is your name?'}]"

[{"detectedLanguage":{"language":"en","score":1.0},"translations":[{"text":"你好，你叫什么名字？","to":"zh-Hans"}]}]%
</code></pre>
<p>大家可以根据请求和应答的JSON结构，结合一些json-to-struct工具自行实现Azure翻译API的Go代码。</p>
<p>对于源文章是中文的，我们可以无需调用该API进行翻译，下面是一个判断字符串是否为中文的函数：</p>
<pre><code>func isChinese(s string) bool {
    for _, r := range s {
        if unicode.Is(unicode.Scripts["Han"], r) {
            return true
        }
    }
    return false
}
</code></pre>
<h2>4. 页面样式设计与html生成</h2>
<p>这次Gopher Daily改版，我为Gopher Daily提供了<a href="https://gopherdaily.tonybai.com">Web版</a>和<a href="https://gopherdaily.tonybai.com/subscribe">邮件列表版</a>，但页面设计是我最不擅长的。好在，和四年前相比，IT技术又有了进一步的发展，以ChatGPT为代表的大语言模型如雨后春笋般层出不穷，我可以借助大模型的帮助来为我设计和实现一个简单的html页面了。下图就是这次改版后的第一版页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/gopherdaily-revamped-5.png" alt="" /></p>
<p>整个页面分为四大部分：Go、云原生(与Go关系紧密，程序员相关，架构相关的内容也放在这部分)、AI(当今流行)以及热门工具与项目(目前主要是github trending中每天Go项目的top列表中的内容)。</p>
<p>每一部分每个条目都包含文章标题、文章链接和文章的摘要，摘要的增加可以帮助大家更好的预览文章内容。</p>
<p>html和markdown的生成都是基于Go的template技术，template也是借助<a href="https://claude.ai/chats">claude.ai</a>设计与实现的，这里就不赘述了。</p>
<h2>5. 服务器选型</h2>
<p>以前的Gopher Daily仅是在github上的一个开源项目，大家通过watch来订阅。此外，<a href="https://github.com/bastengao">Basten Gao</a>维护着一个第三方的<a href="https://gopher-daily.com/">邮件列表</a>，在此也对Basten Gao对Gopher Daily的长期支持表示感谢。</p>
<p>如今改版后，我原生提供了Gopher Daily的Web版，我需要为Gopher Daily选择服务器。</p>
<p>简单起见，我选用了github page来承载Gopher Daily的Web版。</p>
<p>至于邮件列表的订阅、取消订阅，我则是开发了一个小小的服务，跑在<a href="https://m.do.co/c/bff6eed92687">Digital Ocean的VPS</a>上。</p>
<p>在选择反向代理web服务器时，我放弃了nginx，选择了同样Go技术栈实现的<a href="https://github.com/caddyserver/caddy">Caddy</a>。Caddy最大好处就是易上手，且默认自动支持HTTPS，我无需自行用工具向免费证书机构(如 Let&#8217;s Encrypt或ZeroSSL)去申请和维护证书。</p>
<h2>6 小结</h2>
<p>这次改版后的Gopher Daily应得上那句话：“麻雀虽小，五脏俱全”：我为此开发了三个工具，一个服务。</p>
<p>当然Gopher Daily还在持续优化，后续也会根据Gopher们的反馈作适当调整。</p>
<p>摘要和翻译目前使用Azure API，后续可能会改造为使用类ChatGPT的API。</p>
<p>此外，<a href="https://public.zsxq.com/groups/51284458844544">知识星球Gopher部落</a>的星友们依然拥有“先睹为快”的权益。</p>
<p>本文示例代码可以在<a href="https://github.com/bigwhite/experiments/blob/master/gopherdaily-revamped">这里</a>下载。</p>
<ul>
<li><a href="https://gopherdaily.tonybai.com">Gopher Daily网页版</a> &#8211; https://gopherdaily.tonybai.com</li>
<li><a href="https://gopherdaily.tonybai.com/subscribe">Gopher Daily邮件列表订阅</a> &#8211; https://gopherdaily.tonybai.com/subscribe</li>
<li><a href="https://github.com/bigwhite/gopherdaily">Gopher Daily项目归档(markdown版本)</a> &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/08/06/gopherdaily-revamped/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>GoCN社区Go读书会第二期：《Go语言精进之路》</title>
		<link>https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master/</link>
		<comments>https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master/#comments</comments>
		<pubDate>Thu, 07 Jul 2022 11:40:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Amazon]]></category>
		<category><![CDATA[atomic]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[B站]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[delve]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[Function]]></category>
		<category><![CDATA[functrace]]></category>
		<category><![CDATA[fuzzing]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-coding-in-go-way]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go-test]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[GoCN]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GODEUG]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gopherchina]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[Go语言学习笔记]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[Haskell]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[if]]></category>
		<category><![CDATA[init]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[iota]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[leanpub]]></category>
		<category><![CDATA[Method]]></category>
		<category><![CDATA[metrics]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[pitfalls]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[receiver]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[strings]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[trap]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[亚马逊]]></category>
		<category><![CDATA[代码风格]]></category>
		<category><![CDATA[作用域]]></category>
		<category><![CDATA[共享内存]]></category>
		<category><![CDATA[内存分配]]></category>
		<category><![CDATA[写书]]></category>
		<category><![CDATA[出书]]></category>
		<category><![CDATA[出版]]></category>
		<category><![CDATA[函数]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[包导入]]></category>
		<category><![CDATA[华章书院]]></category>
		<category><![CDATA[原子操作]]></category>
		<category><![CDATA[反射]]></category>
		<category><![CDATA[变量声明]]></category>
		<category><![CDATA[坑]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[复合字面值]]></category>
		<category><![CDATA[字符串]]></category>
		<category><![CDATA[导读]]></category>
		<category><![CDATA[常量]]></category>
		<category><![CDATA[并发]]></category>
		<category><![CDATA[并发编程]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[文字稿]]></category>
		<category><![CDATA[方法]]></category>
		<category><![CDATA[机械工业出版社]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[泛读]]></category>
		<category><![CDATA[直播]]></category>
		<category><![CDATA[简单]]></category>
		<category><![CDATA[精读]]></category>
		<category><![CDATA[组合]]></category>
		<category><![CDATA[编程思维]]></category>
		<category><![CDATA[自助出版]]></category>
		<category><![CDATA[自媒体]]></category>
		<category><![CDATA[自带电池]]></category>
		<category><![CDATA[行动清单]]></category>
		<category><![CDATA[设计哲学]]></category>
		<category><![CDATA[语言]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[错误处理]]></category>
		<category><![CDATA[面向工程]]></category>
		<category><![CDATA[项目布局]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3610</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master 本文是2022年6月26日我在GoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿。本文对直播的内容做了重新整理与修订，供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注GoCN公众号和视频号看剪辑后的视频，也可以在B站GopherChina专区下收看视频回放(https://www.bilibili.com/video/BV1p94y1R7jg)。 大家晚上好，我叫白明，是《Go语言精进之路》一书的作者，也是tonybai.com的博主，很荣幸今天参加GoCN社区Go读书会第二期，分享一下我个人在写书和读书方面的经验和体会。 今天的分享包括三方面内容： 写书的历程。一些Gopher可能比较好奇，这么厚的一套书是怎么写出来的，今天就和大家聊一聊。 《Go语言精进之路》导读。主要是把这本书的整体构思与大家聊聊，希望通过这个导读帮助读者更好地阅读和理解这套书。 我个人的读书方法与经验的简要分享。 首先和大家分享一下写书的历程。 一. 写书的历程 1. 程序员的“小目标”与写书三要素 今天收看直播的童鞋都是有追求的技术人员，可能心底都有写一本属于自己的书的小目标。这样可以把自己学习到的知识、技能和经验以比较系统的方式输出给其他人，可以帮助其他人快速学习和掌握本领域的知识、技能和经验。 当然写书还有其他好处，比如：提升名气、更容易混技术圈子、可能给你带来更好的职业发展机会，当然也会给你带来一些额外的副业收入，至于多少，还要看书籍的口碑与销量。 那怎么才能写书呢？作为“过来人”，我总结了三个要素，也是三个条件。 第一个要素是能力。 这个很容易理解。以Go为例，如果你没有在Go语言方面的知识、技能的沉淀，没有对Go语言方方面面的较为深入的理解，你很难写出一本口碑很好的书籍。尤其是那种有原创性、独到见解的著书。而不是对前人资料做系统整理摘抄的编书。编书更常见于教材、字典等。显然著书对作者水平的要求更高。 第二个要素是意愿。 写过书的同学都有体会，写书是一件辛苦活。需要你在正式工作之余付出大量业余时间伏案创作。并且对于小众技术类书籍来说，写书能带来的金钱上的收益和你付出的时长和精力不成正比。就这个问题，我曾与机械工业出版社的营销编辑老师聊过，得到的信息是：Go技术书籍的市场与Java、Python还没法比，即便是像Go语言圣经《Go程序设计语言》的销量也没法与Java、Python的头部书籍销量相比。 第三个要素是机会。 记得小时候十分羡慕那些能出书的人，觉得都是大神级的人物。不过那个时候出书的确很难，机会应该很少，你要不是在学术圈里混很难出书。如今就容易地多了，渠道也多了。每年出版社都有自己的出版计划，各个出版社的编辑老师也在根据计划在各种自媒体上、技术圈子中寻觅匹配的技术作者。 如果你有自己的思路，也可以整理出大纲，并通过某种方式联系到出版社老师，如果匹配就可以出。 另外国外流行电子自助出版，这也给很多技术作者很好的出版机会。比如国内作者老貘写的Go 101系列就是在亚马逊和leanpub上做的自助出版，效果还不错。 以上就是我总结的出书的三个要素，一旦集齐这三个要素呢，出书实际就是自然而然的一件事了。以我为例。 从能力方面来说呢，我大约从2011年开始接触和学习Go语言，算是国内较早的一批Go语言接纳者。Go语言2012年才发布1.0版本，因此那时我接触的Go时还是r60版本，还不是正式的1.0版本。从那时起就一直在跟踪Go演化，日常写一些Go项目的小程序。 Go 1.5实现自举并大幅降低GC延迟，我于是开始在一些生产环境使用Go，并逐渐将知识和经验做了沉淀，在自己的博客上不断做着Go相关内容的输出，反响也不错。 随着输出Go内容的增多，我发现以博客的形式输出，内容组织零散，于是我第一次有了将自己的Go知识系统整理并输出的意愿和想法。 我在实践Go的过程中收到很多Go初学者的提问：Go入门容易，但精进难，怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢？这个问题引发了我的思考。在2017年GopherChina大会我以《go coding in go way》为主题，以演讲的形式尝试回答这个问题，但鉴于演讲的时长有限，很多内容没能展开，效果不甚理想。这进一步增强了我通过书籍的形式系统解答这个问题的意愿。 而当时我家大宝已经长大了，我也希望通过写书这个行动身体力行地给孩子树立一个正面的榜样。中国古语有云：言传身教，我也想践行一下。 机会就这样自然而然的来了！2018年初，机械工业出版社副总编杨福川老师在微信联系到我，和我探讨一下是否可以写一本类似于“Effective Go”的书，当时机械工业出版社华章出版社策划了Effective XXX(编写高质量XXX)系列图书，当时已经出版了C、Python等语言版本的书籍，还差Go语言的。我的出书意愿与出版社的需求甚是匹配，于是我答应的杨老师的要求，成为了这套丛书的Go版本的作者。 2. 写书的过程 我是2018下旬开始真正动笔的。 真正开始码字的时候，我才意识到，写书真不容易，要写出高质量书稿，的确需付出大量时间和汗水。每天晚上、早上都在构思、码字、写代码示例、画插图，睡眠时间很少。记得当时每周末都在奋笔疾书，陪伴家人尤其是孩子的时间很少。 另外我这个人还习惯于把一个知识点讲细讲透，这样每一节的篇幅都不小。因此，写作进展是很缓慢的，就这样，进度一再延期。好在编辑老师比较nice，考虑到书稿质量，没有狠狠催进度。 2020年11月末，我正式向出版社交了初稿，记得初稿有66条，近40w字。 又经过一年的排期、编辑、修订、排版，2021年12月下旬正式出版。 2022年1月《Go语言精进之路》正式上架到各个渠道货架。 到今天为止，出版了近六个月，这本书收获了还不错的口碑，在各个平台上的口碑都在8分以上(注：口碑分数还在动态变化，下图仅为当时的快照，不代表如今的分数)。 能获得大家的认可，让我很是欣慰，觉得写书过程付出的辛苦没有白费。 以上就是我的写书历程。总的来说一句话：写书不易，写高质量的书更难。 接下来我来进行一下《Go语言精进之路》一书的导读。 二. 《Go语言精进之路》导读 也许是“用力过猛”，《Go语言精进之路》一书写的太厚了，无法装订为一册。编辑老师建议装订为两册，即1、2册。很多同学好奇为什么不是上下册而是1、2册，这里是编辑老师的“高瞻远瞩”，目的是为后续可能的“续写”(比如第3册)留足空间，毕竟Go语言还在快速演进，目前的版本还不包含像泛型这样的新语法。不过，目前第3册还尚未列入计划。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master">本文永久链接</a> &#8211; https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master</p>
<p>本文是2022年6月26日我在<strong>GoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿</strong>。本文对直播的内容做了重新整理与修订，供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注<strong>GoCN公众号和视频号</strong>看剪辑后的视频，也可以<a href="https://www.bilibili.com/video/BV1p94y1R7jg">在B站GopherChina专区下收看视频回放</a>(https://www.bilibili.com/video/BV1p94y1R7jg)。</p>
<hr />
<p>大家晚上好，我叫白明，是<a href="https://item.jd.com/13694000.html">《Go语言精进之路》</a>一书的作者，也是<a href="https://tonybai.com">tonybai.com</a>的博主，很荣幸今天参加GoCN社区Go读书会第二期，分享一下我个人在写书和读书方面的经验和体会。</p>
<p>今天的分享包括三方面内容：</p>
<ul>
<li>写书的历程。一些Gopher可能比较好奇，这么厚的一套书是怎么写出来的，今天就和大家聊一聊。</li>
<li>《Go语言精进之路》导读。主要是把这本书的整体构思与大家聊聊，希望通过这个导读帮助读者更好地阅读和理解这套书。</li>
<li>我个人的读书方法与经验的简要分享。</li>
</ul>
<p>首先和大家分享一下写书的历程。</p>
<h2>一. 写书的历程</h2>
<p><img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-2.png" alt="" /></p>
<h3>1. 程序员的“小目标”与写书三要素</h3>
<p>今天收看直播的童鞋都是有追求的技术人员，可能心底都有写一本属于自己的书的小目标。这样可以把自己学习到的知识、技能和经验以比较系统的方式输出给其他人，可以帮助其他人快速学习和掌握本领域的知识、技能和经验。</p>
<p>当然写书还有其他好处，比如：提升名气、更容易混技术圈子、可能给你带来更好的职业发展机会，当然也会给你带来一些额外的副业收入，至于多少，还要看书籍的口碑与销量。</p>
<p>那怎么才能写书呢？作为“过来人”，我总结了三个要素，也是三个条件。</p>
<p>第一个要素是<strong>能力</strong>。</p>
<p>这个很容易理解。以Go为例，如果你没有在Go语言方面的知识、技能的沉淀，没有对Go语言方方面面的较为深入的理解，你很难写出一本口碑很好的书籍。尤其是那种有原创性、独到见解的著书。而不是对前人资料做系统整理摘抄的编书。编书更常见于教材、字典等。显然著书对作者水平的要求更高。</p>
<p>第二个要素是<strong>意愿</strong>。</p>
<p>写过书的同学都有体会，写书是一件辛苦活。需要你在正式工作之余付出大量业余时间伏案创作。并且对于小众技术类书籍来说，写书能带来的金钱上的收益和你付出的时长和精力不成正比。就这个问题，我曾与机械工业出版社的营销编辑老师聊过，得到的信息是：Go技术书籍的市场与Java、Python还没法比，即便是像Go语言圣经《Go程序设计语言》的销量也没法与Java、Python的头部书籍销量相比。</p>
<p>第三个要素是<strong>机会</strong>。</p>
<p>记得小时候十分羡慕那些能出书的人，觉得都是大神级的人物。不过那个时候出书的确很难，机会应该很少，你要不是在学术圈里混很难出书。如今就容易地多了，渠道也多了。每年出版社都有自己的出版计划，各个出版社的编辑老师也在根据计划在各种自媒体上、技术圈子中寻觅匹配的技术作者。</p>
<p>如果你有自己的思路，也可以整理出大纲，并通过某种方式联系到出版社老师，如果匹配就可以出。</p>
<p>另外国外流行电子自助出版，这也给很多技术作者很好的出版机会。比如国内作者老貘写的<a href="https://go101.org/">Go 101系列</a>就是在<a href="https://kdp.amazon.com/en_US/">亚马逊</a>和<a href="https://leanpub.com">leanpub</a>上做的自助出版，效果还不错。</p>
<p>以上就是我总结的出书的三个要素，一旦集齐这三个要素呢，出书实际就是自然而然的一件事了。以我为例。</p>
<p>从能力方面来说呢，我大约从2011年开始接触和学习Go语言，算是国内较早的一批Go语言接纳者。Go语言2012年才发布1.0版本，因此那时我接触的Go时还是r60版本，还不是正式的1.0版本。从那时起就一直在跟踪Go演化，日常写一些Go项目的小程序。</p>
<p>Go 1.5实现自举并大幅降低GC延迟，我于是开始在一些生产环境使用Go，并逐渐将知识和经验做了沉淀，在自己的博客上不断做着Go相关内容的输出，反响也不错。</p>
<p>随着输出Go内容的增多，我发现以博客的形式输出，内容组织零散，于是我第一次有了将自己的Go知识系统整理并输出的意愿和想法。</p>
<p>我在实践Go的过程中收到很多Go初学者的提问：Go入门容易，但精进难，怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢？这个问题引发了我的思考。在2017年GopherChina大会我以<a href="https://tonybai.com/2017/04/20/go-coding-in-go-way/">《go coding in go way》</a>为主题，以演讲的形式尝试回答这个问题，但鉴于演讲的时长有限，很多内容没能展开，效果不甚理想。这进一步增强了我通过书籍的形式系统解答这个问题的意愿。</p>
<p>而当时我家<a href="https://daughter.tonybai.com">大宝</a>已经长大了，我也希望通过写书这个行动身体力行地<strong>给孩子树立一个正面的榜样</strong>。中国古语有云：<strong>言传身教</strong>，我也想践行一下。</p>
<p>机会就这样自然而然的来了！2018年初，机械工业出版社副总编杨福川老师在微信联系到我，和我探讨一下是否可以写一本类似于“Effective Go”的书，当时机械工业出版社华章出版社策划了Effective XXX(编写高质量XXX)系列图书，当时已经出版了C、Python等语言版本的书籍，还差Go语言的。我的出书意愿与出版社的需求甚是匹配，于是我答应的杨老师的要求，成为了这套丛书的Go版本的作者。</p>
<h3>2. 写书的过程</h3>
<p>我是2018下旬开始真正动笔的。</p>
<p>真正开始码字的时候，我才意识到，写书真不容易，要写出高质量书稿，的确需付出大量时间和汗水。每天晚上、早上都在构思、码字、写代码示例、画插图，睡眠时间很少。记得当时每周末都在奋笔疾书，陪伴家人尤其是孩子的时间很少。</p>
<p>另外我这个人还习惯于把一个知识点讲细讲透，这样每一节的篇幅都不小。因此，写作进展是很缓慢的，就这样，进度一再延期。好在编辑老师比较nice，考虑到书稿质量，没有狠狠催进度。</p>
<p>2020年11月末，我正式向出版社交了初稿，记得初稿有66条，近40w字。</p>
<p>又经过一年的排期、编辑、修订、排版，2021年12月下旬正式出版。</p>
<p>2022年1月《Go语言精进之路》正式上架到各个渠道货架。</p>
<p>到今天为止，出版了近六个月，这本书收获了还不错的口碑，在各个平台上的口碑都在8分以上(注：口碑分数还在动态变化，下图仅为当时的快照，不代表如今的分数)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-5.jpg" alt="" /><br />
<img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-6.png" alt="" /></p>
<p>能获得大家的认可，让我很是欣慰，觉得写书过程付出的辛苦没有白费。</p>
<p>以上就是我的写书历程。总的来说一句话：<strong>写书不易，写高质量的书更难</strong>。</p>
<p>接下来我来进行一下《Go语言精进之路》一书的导读。</p>
<h2>二. 《Go语言精进之路》导读</h2>
<p><img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-3.png" alt="" /></p>
<p>也许是“用力过猛”，《Go语言精进之路》一书写的太厚了，无法装订为一册。编辑老师建议装订为两册，即1、2册。很多同学好奇为什么不是上下册而是1、2册，这里是编辑老师的“高瞻远瞩”，目的是为后续可能的“续写”(比如第3册)留足空间，毕竟Go语言还在快速演进，目前的版本还不包含像<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">泛型这样的新语法</a>。不过，目前第3册还尚未列入计划。</p>
<p>本套书共分为10个部分，66个主题。第一册包含了前7个部分，后3部分在第二册中。</p>
<h3>1. 整体写作思路</h3>
<p>整套书围绕着两个前后关联的思路循序展开。</p>
<p>第一个思路我叫它：<strong>精进之路，思维先行</strong>。</p>
<p>第二个思路称为：<strong>践行哲学，遵循惯例，认清本质，理解原理</strong>。</p>
<p>我们先来看看第一个思路。</p>
<h3>2. 精进之路，思维先行</h3>
<p>收看直播的童鞋都不止学过一门编程语言。大家可能都有过这样的经历：你已经精通A语言，然后在学习B语言的时候用A语言的思维去写B代码，你会觉得写出的B代码很别扭，写出的代码总是感觉不是很地道，总觉得不是那种高质量的B语言代码。</p>
<p>其实，不仅学习编程语言是这样，学自然语言也是一样。最典型的一个例子，大家都学过十几年的英语，但毕业后能用地道的英语表达自己观点的人却不多，为什么呢？那就是我们总用中文的思维方式去组织英语的句子，去说英语，这样再怎么努力也很难上一个层次。</p>
<p>其实，很多语言大师早就意识到了这一点。下面是我收集的这些大师的关于语言与思维的论点，这里和大家分享一下：</p>
<blockquote>
<p>“语言决定思维方式” &#8211; 萨丕尔假说</p>
<p>“我的语言之局限，即我的世界之局限” &#8211;  路德维希·维特根斯坦，语言哲学的奠基人</p>
<p>“不能改变你思维方式的语言，不值得学习” &#8211; Alan Perlis（首届ACM图灵奖得主)</p>
</blockquote>
<p>我们看到：无论是自然语言界的大师，还是IT界的大佬，他们的观点异曲同工。总之一句话：<strong>语言要精进，思维要先行</strong>。</p>
<h3>3. Part1：进入Go语言编程思维导引</h3>
<p>正是因为意识到语言与思维的紧密关系，我在书的第一部分就安排了Go语言编程思维导引，希望大家意识到Go编程思维在语言精进之路上的重要性。</p>
<p>一门编程语言的思维也不是与生俱来的，而是在演进中逐步形成的。所以在这一部分，我安排了Go诞生与演进、Go设计哲学：简单、组合、并发、面向工程。这样做的目的是让大家一起了解Go语言设计者在设计Go语言时的所思所想，让读者站在语言设计者的高度理解Go语言与众不同的设计，认同Go语言的设计理念。因为这些是<strong>Go编程语言思维形成的“土壤”</strong>。</p>
<p>这一部分最后一节是Go编程思维举例导引，书中给出了C, Haskell和Go程序员在面对同一个问题时，首先考虑到的思维方式以及不同思维下代码设计方式的差异。</p>
<p>知道Go编程思维的重要性后，我们应该怎么做呢？</p>
<h3>4. 怎么学习Go编程思维？</h3>
<p>学习的本质是一种模仿。<strong>要学习Go思维，就要去模仿Go团队、Go社区的优秀项目和代码，看看他们怎么做的</strong>。这套书后面的部分讲的就是这个。而“践行哲学，遵循惯例，认清本质，理解原理”就是对后面内容的写作思路的概要性总结。</p>
<ul>
<li>践行哲学</li>
</ul>
<p>把Go设计哲学用于自己的项目的设计实践中，而不是仅停留在口头知道上。</p>
<ul>
<li>遵循惯例</li>
</ul>
<p>遵循Go团队的一些语言惯例，比如“comma，ok”、使用复合字面值初始化等，使用这些惯例你可以让你的代码显得很地道，别人一看就懂。</p>
<ul>
<li>认清本质</li>
</ul>
<p>为了更高效地利用语言机制，我们要认清一些语言机制背后的本质，比如切片、字符串在运行时的表示，这样一来既能帮助开发人员正确使用这些语法元素，同时也能避免入坑。</p>
<ul>
<li>理解原理</li>
</ul>
<p>Go带有运行时。运行时全程参与Go应用生命周期，因此，只有对Goroutine调度、GC等原理做适当了解，才能更好的发挥Go的威力。</p>
<p>这套书的part2-part10 就是基于对Go团队、Go社区优秀实践与惯例的梳理，用系统化的思路构建出来并循序渐进呈现给大家的。</p>
<h3>5. Part2 – 项目基础：布局、代码风格与命名</h3>
<p>这部门的内容是每个gopher在开启一个Go项目时都要考虑的事情。</p>
<ul>
<li>项目布局</li>
</ul>
<p>我见过很多Gopher问项目布局的事情，因为Go官方没有给出标准布局。本书讲解了Go项目的结构布局的演进历程以及Go社区的事实标准，希望能给大家提供足够的参考信息。</p>
<ul>
<li>代码风格</li>
</ul>
<p>针对Go代码风格，由于代码风格在Go中已经弱化，所以这里主要还是带大家理解gofmt存在的意义和使用方法。</p>
<ul>
<li>命名惯例</li>
</ul>
<p>关于命名，我不知道大家是否觉得命名难，但对我来说是挺难的，我总是绞尽脑汁在想用啥名(手动允悲)。所以我的原则是“代码未动，命名先行”。 对于Go中变量、标识符等的命名惯例这样的“关键的问题”，我使用了“笨方法”：我统计了Go标准库、Docker库、k8s库的命名情况，并分门别类给出不同语法元素的命名惯例，具体内容大家可以看书了解 。</p>
<h3>6. Part3 – 语法基础：声明、类型、语句与控制结构</h3>
<p>第三部分讲的很基础，但内容还是要高于基础的。</p>
<ul>
<li>一致的变量声明</li>
</ul>
<p>我们知道Go提供多种变量声明方式，但是在不同位置该用哪种声明方式可读性好又不容易造坑呢(尤其要注意短变量声明)？书中给出了系统阐述。</p>
<ul>
<li>无类型常量与iota</li>
</ul>
<p>大家都用过常量，但很多人对于无类型常量与有类型常量区别不了解，书中帮你做了总结。还有，很多人用过iota，但却不理解iota的真正含义以及它能帮你做啥。书中对iota的语义做了说明，对常见用途做了梳理。</p>
<ul>
<li>零值可用</li>
</ul>
<p>Go提倡零值可用，也内置了有很多零值可用类型，用起来很爽，比如：切片(不全是，仅在append时是零值可用，当用下标访问时，不具备零值可用)、sync包中的Mutex、RDMutex等</p>
<p>其实类比于线程（thread），goroutine也是一种零值可用的“类型”，只是Go没有goroutine这个类型罢了。</p>
<p>如果我们是包的设计者，如果提供零值可用的类型，可以提升包的使用者的体验。</p>
<ul>
<li>复合字面值来初始化</li>
</ul>
<p>使用复合字面值对相应的变量进行初始化是一个Go语言的惯例， Go虽然提供了new和make，但日常很少用，尤其是new。</p>
<ul>
<li>切片、字符串、map的原理、惯用法与坑</li>
</ul>
<p>Go是带有runtime的语言，语法层面展示的很多语法元素和runtime层真实的表示并不一致。要想高效利用这些类型，如果不了解runtime层表示还真不行。有时候还有很严重的“坑”。懂了，自然就能绕过坑。</p>
<ul>
<li>包导入</li>
</ul>
<p>Go源文件的import语句后面跟着的是包名还是包路径？Go编译是不是必须要有依赖项的源码才可以，只有.a是否可以？这些问题书中都有系统说明</p>
<ul>
<li>代码块与作用域</li>
</ul>
<p>代码块与作用域是Go语言的基础概念，虽然基础，如果理解不好，也是有“坑”的，比如最常见的变量遮蔽等。一旦理解透了，还可以帮你解决意想不到的语法问题和执行语义错误问题。</p>
<ul>
<li>控制语句</li>
</ul>
<p>Go倡导“一个问题只有一种解决方法”。Go针对每种控制语句仅提供一种语法形式。虽然仅有一种形式，用不好，一样容器掉坑。本套书总结了Go控制语句的惯用法与使用注意事项。</p>
<h3>7. Part4 – 语法基础：函数与方法</h3>
<p>我们日常编写的Go代码逻辑都在函数或方法中，函数/方法是Go程序逻辑的基本承载单元。</p>
<ul>
<li>init函数</li>
</ul>
<p>init函数是包初始化过程中执行的函数，它有很多特殊用途。并且其初始化顺序对程序执行语义也有影响，这方面要搞清楚。书中对init函数的常见用途做了梳理，比如database/sql包的驱动自注册模式等。</p>
<ul>
<li>成为“一等公民” </li>
</ul>
<p>在Go中，函数成为了“一等公民”。函数成为一等公民后可以像变量一样，被作为参数传递到函数中、作为返回值从函数中返回、作为右值赋值给其他变量等，书中系统讲解了这个特性都有哪些性质和特殊应用，比如函数式编程等。</p>
<ul>
<li>defer语句的惯用法与坑</li>
</ul>
<p>defer就是帮你简化代码逻辑的，书中总结了defer语句的应用模式。以及使用defer的注意事项，比如函数求值时机、使用开销等。</p>
<ul>
<li>变长参数函数</li>
</ul>
<p>Go支持变长参数函数。大家可以没有意识到：变长参数函数是我们日常用的最多的一类函数，比如append函数、fmt.Printf系列、log包中提供的按日志严重级别输出日志的函数等。</p>
<p>但变长参数函数可能也是我们自己设计与实现较少的一类函数形式。 变长参数函数能帮我们做什么呢？书中讲解了变长参数函数的常见用途，比如实现功能选项模式等。</p>
<ul>
<li>方法的本质、receiver参数类型选择、方法集合</li>
</ul>
<p>方法的本质其实是函数，弄清楚方法的本质可以帮助我们解决很多难题，书中以实例方式帮助大家理解这一点。</p>
<p>方法receiver参数类型的选择也是Go初学者的常见困惑，这里书中给出三个原则，参照这三个原则，receiver类型选择就不是问题了。</p>
<p>怎么确定一个类型是否实现接口？我们需要看类型的方法集合。那么确定一个类型方法集合就十分重要，尤其是那些包括类型嵌入的类型的方法集合，书中对这块内容做了系统的讲解。</p>
<h3>8. Part5 – 语法核心：接口</h3>
<ul>
<li>接口的内部表示</li>
</ul>
<p>接口是Go语言中的重要语法。Russ Cox曾说过：“如果要从Go语言中挑选出一个特性放入其他语言，我会选择接口”。可见接口的重要性。不过，用好接口类型的前提是理解接口在runtime层的表示，这一节会详细说明空接口与非空接口的内部表示。</p>
<ul>
<li>接口的设计惯例</li>
</ul>
<p>我们应该设计什么样的接口呢？ 大接口有何弊端？小接口有何优势？多小的接口算是合理的呢？这些在本节都有说明。</p>
<ul>
<li>接口与组合</li>
</ul>
<p>组合是Go的设计哲学，Go是关于组合的语言。接口在面向组合编程时将发挥重要作用。这里我将提到Go的两种组合方式：垂直组合和水平组合。其中接口类型在水平组合中起到的关键性的作用。书中还讲解了通过接口进行水平组合的几种模式：包裹模式、适配器函数、中间件等。</p>
<p>很多初学者告诉我，他们做了一段时间Go编码了，但还没有自己设计过接口，我建议这样的同学好好读读这一部分。</p>
<h3>9. Part6 – 语法核心：并发编程</h3>
<ul>
<li>并发设计vs并行设计</li>
</ul>
<p>学习并发编程首先要搞懂并发与并行的概念，书中用了一个很形象的机场安检的例子，来告诉大家并发与并行的区别。并发关乎结构，并行关注执行</p>
<ul>
<li>并发原语的原理与应用模式</li>
</ul>
<p>Go实现了csp模型，提供了goroutine、channel、select并发原语。</p>
<p>理解go并发编程。首先要深入理解基于goroutine的并发模型与调度方式。书中对这方面做了深入浅出的讲解，不涉及太多代码，相信大家都能看懂。</p>
<p>书中还对比了go并发模型，一种是csp，一种是传统的基于共享内存方式，并列举了Go并发的常见模式，比如创建、取消、超时、管道模式等。</p>
<p>另外，channel作为goroutine间通信的标准原语，有很多玩法，这里列举了常见的模式和使用注意事项。</p>
<ul>
<li>低级同步原语(sync和atomic)</li>
</ul>
<p>虽然有了CSP模型的并发原语，极大简化并发编程，但是sync包和原子操作也不能忘记，很多性能敏感的临界区还需要sync包/atomic这样的低级同步原语来同步。</p>
<h3>10. Part7 – 错误处理</h3>
<p>单独将错误处理拎出来，是因为很多人尤其是来自java的童鞋，习惯了try-catch-finally的结构化错误处理，看到go的错误处理就让其头疼。</p>
<p>Go语言十分重视错误处理，但它也的确有着相对保守的设计和显式处理错误的惯例。</p>
<p>本部分涵盖常见Go错误处理的策略、避免if err != nil写太多的方案，更为重要的是panic与错误处理的差别。我见过太多将panic用作正常处理的同学了。尤其是来自java阵营的童鞋。</p>
<h3>11. Part8 – 编程实践：测试、调试与性能剖析</h3>
<p>本部分聚焦编码之外的Go工具链工程实践。</p>
<ul>
<li>Go测试惯例与组织形式 </li>
</ul>
<p>这部分首先和大家聊聊go test包的组织形式，包括是选择包内测试还是包外测试？何时采用符合go惯例的表驱动的测试用例组织形式？如何管理测试依赖的外部数据文件等。</p>
<ul>
<li>模糊测试(fuzzing test)。</li>
</ul>
<p>这里的模糊测试并非基于go 1.18的原生<a href="https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18">fuzzing test</a>进行，写书的时候go 1.18版本尚未发布，而是基于德米特里-维尤科夫的<a href="http://tonybai.com/2015/12/08/go-fuzz-intro/">go-fuzz工具</a>。</p>
<ul>
<li>性能基准测试、度量数据与pprof性能剖析</li>
</ul>
<p>Go原生提供性能基准测试。这一节讲解了如何做性能基准测试、如何编写串行与并行的测试、性能基准测试结果比较工具以及如何排除额外干扰，让结果更准确等方面内容。在讲解pprof性能剖析工具时，我使用一个实例进行剖析讲解，这样理解起来更为直观。</p>
<ul>
<li>Go调试</li>
</ul>
<p>说到Go调试，我们日常使用最多的估计还是print大法。但在print大法之外，其实有一个事实标准的Go调试工具，它就是delve。在这一节中，我讲解了delve的工作原理以及使用delve如何实现并发调试、coredump调试以及在线挂接(attach)进程的调试。</p>
<h3>12. Part9 – 标准库、反射与cgo</h3>
<p>go是自带电池，开箱即用的语言，拥有高质量的标准库。在国外有些Gopher甚至倡导仅依赖标准库实现go应用。</p>
<ul>
<li>高频使用的标准库包（net、http、strings、time、crypto等)</li>
</ul>
<p>在这一节，我对高频使用的标准库包的原理和使用进行拆解分析，net、http、标准库io模型、strings、time、crypto等以帮助大家更高效的运用标准库。</p>
<ul>
<li>reflect包使用的三大法则</li>
</ul>
<p>reflect包为go提供了反射能力，书中对反射的实现原理做了讲解，重点是reflect使用的三大法则。</p>
<ul>
<li>cgo使用</li>
</ul>
<p>cgo不是go，但是cgo机制是使用go与c交互的唯一手段。书中对cgo的用法与约束做了详细讲解，尤其是在cgo开启的情况下如何做静态编译值得大家细读。</p>
<ul>
<li>unsafe包的安全使用法则</li>
</ul>
<p>事实证明unsafe包很有用，但要做到安全使用unsafe包，尤其是unsafe.Pointer，需要遵循一定的安全使用法则。书中对此做了举例详细说明。</p>
<p>反射、cgo、unsafe算是高级话题，要透彻理解，需要多阅读几遍书中内容并结合实践。</p>
<h3>13. Part10 – 工程实践</h3>
<ul>
<li>go module</li>
</ul>
<p>go module在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">go 1.11版本</a>中引入go，在<a href="https://tonybai.com/2021/02/25/some-changes-in-go-1-16">go 1.16版本</a>中成为go官方默认构建模式。go程序员入门go，精进go都跨不过go module这道坎儿。书中对go module构建模式做了超级系统的讲解：从go构建模式演进历史、go module的概念、原理、惯例、升降级major版本的操作，到使用注意事项等。不过这里还有有一些瑕疵，那就是go module这一节放置的位置太靠后了，应该往往前面提提。如果后面有修订版，可以考虑这么做。</p>
<ul>
<li>自定义go包导入路径</li>
</ul>
<p>书中还给出了一个自定义go包导入路径的一种实现方案，十分适合组织内部的私有仓库，有兴趣的同学可以重点看看。</p>
<ul>
<li>go命令的使用模式详解</li>
</ul>
<p>这一节将go命令分门别类地进行详细说明。包括：</p>
<pre><code>- 获取与安装的go get/go install
- go包检视的go list
- go包构建的go build
- 运行与诊断的GODEBUG、GOGC等环境变量的功用
- 代码静态检查与重构
- 文档查看
- go代码生成go generate
</code></pre>
<ul>
<li>Go常见的“坑”</li>
</ul>
<p>这一节将Go常见的“坑”进行了一次检阅。我这里将坑分为“语法类”和“标准库类”，并借鉴了央视五套天下足球top10节目，对每个坑的“遇坑指数”与“坑害指数”做了点评。</p>
<h3>14. 具备完整的示例代码与勘误表</h3>
<p>这套书拥有具备完整的示例代码与<a href="https://github.com/bigwhite/GoProgrammingFromBeginnerToMaster">勘误表</a>，它们都被持续维护，让大家没有读书的后顾之忧。</p>
<h2>三. 读书的实践与体会</h2>
<p><img src="https://tonybai.com/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-4.png" alt="" /></p>
<p>下面我再分享一下我个人是怎么读书的，包括go技术书籍的读书历程，以及关于读书的一些实践体会。</p>
<p>读书是千人千面的事，没有固定标准的。我的读书方法也不见得适合诸位。大家听听即可，觉得还不错，能借鉴上就最好了。</p>
<p>今天收看直播估计以gopher为主，所以首先说说Go语言书籍的阅读历程</p>
<h3>1. Go语言书籍阅读历程：先外后内</h3>
<p>对于IT技术类图书，初期还是要看原版的。这个没办法，因为it编程技术绝大多数来自国外。</p>
<p>我读的第一本Go技术书就是《the way to go》，至今这本书也没有引入国内。这是一本Go语言百科全书，大多数内容如今仍适用。唯一不足是该书成书于Go 1.0发布之前，使用的好像是r60版本，有少部分内容已经不适用。</p>
<p>后来Go 1.0发布后，我还陆续读过Addison-Wesley出版的《programming in go》和《The Go Programming Language Phrasebook》，两本书都还不错。</p>
<p>2015年末的布莱恩.克尼根和go核心团队的多诺万联合编写的《The Go Programming Language》，国内称之为Go圣经的书出版了，这让外文go技术书籍达到了巅峰，后来虽然也有go书籍书籍陆续出版，但都无法触及go圣经的地位。</p>
<p>说完外文图书，我再来说说中文Go图书的阅读历程。</p>
<p>我读过的第一本中文Go书籍是2012年许式伟老师的《Go语言编程》，很佩服许老师的眼光和魄力，七牛云很早就在生产用go。</p>
<p>第二本中文Go书籍是雨痕老师的《go学习笔记》，这也是国内第一本深入到go底层原理的书籍(后半部分)，遗憾的是书籍停留在go 1.5(还是go 1.6)的实现上，没有随Go版本演进而持续更新。</p>
<p>柴大和曹大合著的《go高级编程》也是一本不错的go技术书籍，如果你要深入学习cgo和go汇编，建议阅读此书。</p>
<p>后面的《Go语言底层原理剖析》和《Go语言设计与实现》也都是以深入了解Go运行机制为目标的书籍，口碑都很好，对这方面内容感兴趣的gopher，可以任意挑一本学习。</p>
<h3>2. 自己的读书方法</h3>
<p>我的读书方法其实不复杂，主要分为精读和泛读。</p>
<ul>
<li>阅读方式：好书精读，闲书泛读</li>
</ul>
<p>好书，集中一大段时间内进行阅读。 闲书(不烧脑)，通常是 碎片化阅读。</p>
<ul>
<li>精读方法：摘录+脑图+行动清单</li>
</ul>
<p>摘录就是将书中的观点和细节摘录出来，放到读书笔记，最好能用自己的语言重新描述出来，这样印象深刻，理解更为透彻。</p>
<p>脑图，概括书的思维脉络，防止读完就忘记。 通过脑图，我至少看着脉络能想起来。</p>
<p>行动清单：如果没有能输出行动清单，那这本书对你来说意义就不大。 什么是好书，好书就是那种看完后很迫切的想基于书中的观点做点什么。行动清单将有助于我在后续的行动中反复理解书中内容，提高知识的消化率和理解深度。</p>
<ul>
<li>泛读方法：碎片化+听书</li>
</ul>
<p>泛读主要是碎片化快读或听书，主要是坐地铁，坐公交，散步时。开车时在保证安全的前提下，可以用听书的方式。</p>
<h2>四. 小结</h2>
<p>本次分享了三块内容，这里小结一下：</p>
<ul>
<li>写书历程和写书三要素：能力 + 意愿 + 机会；</li>
<li>Go精进之路导读：思维先行，践行哲学，遵循惯例，认清本质，理解原理；</li>
<li>读书方法：选高质量图书精读(脑图+细节摘录+行动清单）。</li>
</ul>
<h2>五. Q&amp;A</h2>
<ul>
<li>在实际开发中有没有什么优雅的处理error的方法？</li>
</ul>
<p>建议看《Go语言精进之路》第一册第七部分中关于error处理的内容。</p>
<ul>
<li>是否在工作中使用过六边形架构以及依赖注入的处理经验?</li>
</ul>
<p>暂没有使用过六边形架构，生产中没有使用过Go第三方依赖注入的方案。</p>
<ul>
<li>后面会有泛型和模糊测试的补充么？</li>
</ul>
<p>从书籍内容覆盖全面性的角度而言，我个人有补充上述内容的想法，但还要看现在这套书的销售情况以及出版社的计划。目前还没列入个人工作计划。</p>
<ul>
<li>作者总结一系列go方法论、惯例等很实用，这种有逻辑的思考和见解是怎么形成的？</li>
</ul>
<p>没有特意考虑过是怎么形成的。个人平时喜欢多问自己几个为什么，形成让自己信服的工作和学习逻辑。(文字稿补充：同理心、多总结、多复盘、多输出)。</p>
<p>学习Go惯例、方法论，可以多多看Go语言开源项目自身的代码评审，看看Go contributor写代码的思路和如何评审其他贡献者的代码的。(文字稿补充：在这一过程中，潜移默化的感受Go编程思维)。</p>
<ul>
<li>如何阅读大型go项目的源码？</li>
</ul>
<p>我个人的方法就是自上而下。先拆分结构，然后找入口。如果是一个可执行的go程序，还是从入口层层的向后看。然后通过一些工具，比如我个人之前开发的<a href="https://tonybai.com/2020/12/10/a-kind-of-thinking-about-how-to-trace-function-call-chain">函数调用跟踪工具</a>，查看程序执行过程中的函数调用次序。</p>
<p>更细节的内容，还是要深入到代码中去查看。</p>
<ul>
<li>对Go项目中的一些设计模式的看法？如何使用设计模式，使用时注意哪些事项？</li>
</ul>
<p>设计模式在go语言中并不是一个经常拿出来提的东西。我之前的一个观点：在其他语言中，需要大家通过一些额外细心的设计构建出来的设计模式，在Go语言中是自然而然就有的东西。</p>
<p>我在自己的日常编码过程中，不会太多从如何应用设计模式的角度思考，而是按照go设计哲学，去考虑并发设计、组合的设计，而不是非要套用那23个经典设计模式。</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>小厂内部私有Go module拉取方案（续）</title>
		<link>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/</link>
		<comments>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/#comments</comments>
		<pubDate>Sat, 18 Jun 2022 14:10:10 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[access.log]]></category>
		<category><![CDATA[Code-Review]]></category>
		<category><![CDATA[gerrit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.14]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodtidy]]></category>
		<category><![CDATA[GOPRIVATE]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[GOSECURE]]></category>
		<category><![CDATA[govanityurls]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[netrc]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[proxy]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[代码评审]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3593</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2 自从去年在公司搭建了内部私有Go module proxy后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。 日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。 突然有一天，业务线考虑将目前在用的gerrit换成gitlab。最初使用gerrit的原因不得而知，但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快，每个迭代的功能都很多，“+2”的review机制到后来就形同虚设了。 如果不用gerrit review工作流，那么gerrit还有什么存在的价值呢。从管理员那边反馈，gerrit配置起来也是比较复杂的，尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是如何让我们内部私有go module代理适配gitlab。 如果你还不清楚我们搭建私有Go module代理的原理，那么在进一步往下阅读前，请先阅读一下《小厂内部私有Go module拉取方案》。 适配gitlab 回顾一下我们的私有Go module代理的原理图： 基于这张原理图，我们分析后得出结论：要适配gitlab仓库，其实很简单，只需修改govanityurls的配置文件中的各个module的真实repo地址即可，这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。 下面我们在gitlab上创建一个foo repo，其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo，先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址：git@10.10.30.30:go/foo.git填到vanity.yaml文件中： //vanity.yaml ... ... /go/foo: repo: ssh://git@10.10.30.30:go/foo.git vcs: git 我门在一台开发机上建立测试程序，该程序导入mycompany.com/go/foo，执行go mod tidy命令的结果如下： $go mod tidy go: finding module for package mycompany.com/go/foo demo imports mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-part2-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2">本文永久链接</a> &#8211; https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2</p>
<p>自从去年在公司<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">搭建了内部私有Go module proxy</a>后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。</p>
<p>日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。</p>
<p>突然有一天，业务线考虑将目前在用的<a href="https://www.gerritcodereview.com">gerrit</a>换成<a href="https://about.gitlab.com">gitlab</a>。最初使用gerrit的原因不得而知，但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快，每个迭代的功能都很多，“+2”的review机制到后来就形同虚设了。</p>
<p>如果不用gerrit review工作流，那么gerrit还有什么存在的价值呢。从管理员那边反馈，gerrit配置起来也是比较复杂的，尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是<strong>如何让我们内部私有go module代理适配gitlab</strong>。</p>
<blockquote>
<p>如果你还不清楚我们搭建私有Go module代理的原理，那么在进一步往下阅读前，请先阅读一下<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">《小厂内部私有Go module拉取方案》</a>。</p>
</blockquote>
<h3>适配gitlab</h3>
<p>回顾一下我们的私有Go module代理的原理图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-9.png" alt="" /></p>
<p>基于这张原理图，我们分析后得出结论：要适配gitlab仓库，其实很简单，只需修改govanityurls的配置文件中的各个module的真实repo地址即可，这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。</p>
<p>下面我们在gitlab上创建一个foo repo，其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo，先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址：git@10.10.30.30:go/foo.git填到vanity.yaml文件中：</p>
<pre><code>//vanity.yaml
  ... ...
  /go/foo:
     repo: ssh://git@10.10.30.30:go/foo.git
     vcs: git
</code></pre>
<p>我门在一台开发机上建立测试程序，该程序导入mycompany.com/go/foo，执行go mod tidy命令的结果如下：</p>
<pre><code>$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: mycompany.com/go/foo@latest: unrecognized import path "mycompany.com/go/foo": http://mycompany.com/go/foo?go-get=1: invalid repo root "ssh://git@10.10.30.30:go/foo.git": parse "ssh://git@10.10.30.30:go/foo.git": invalid port ":go" after host
</code></pre>
<p>从goproxy返回的response内容来看，似乎是goproxy使用的go命令无法识别：”ssh://git@10.10.30.30:go/foo.git”，认为10.10.30.30后面的分号后面应该接一个端口，而不是go。</p>
<p>我们将repo换成下面这样的格式：</p>
<pre><code>  /go/foo:
     repo: ssh://git@10.10.30.30:80/go/foo.git
     vcs: git
</code></pre>
<p>重启govanityurls并重新执行go mod tidy，依旧报错：</p>
<pre><code>$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: module mycompany.com/go/foo: git ls-remote -q origin in /root/.bin/goproxycache/pkg/mod/cache/vcs/4d37c02c151342112bd2d7e6cf9c0508b31b8fe1cf27063da6774aa0f53d872f: exit status 128:
        kex_exchange_identification: Connection closed by remote host
        fatal: Could not read from remote repository.
</code></pre>
<p>直接在主机上通过git clone git@10.10.30.30:80/go/foo.git也是报错的！ssh不行，我们再来试试http方式。 使用http方式呢，每次clone都需要输入用户名密码，不适合goproxy。是时候让personal token上阵了！在gitlab上分配好personal token，然后在本地建立~/.netrc如下：</p>
<pre><code># cat ~/.netrc
machine 10.10.30.30
login tonybai
password [your personal token]
</code></pre>
<p>然后我们将vanity.yaml中的repo改为如下形式：</p>
<pre><code>// vanity.yaml

  /go/foo:
     repo: http://10.10.30.30/go/foo.git
     vcs: git
</code></pre>
<p>这样再执行go mod tidy，foo仓库就被顺利拉取了下来。</p>
<h3>答疑</h3>
<h4>1. git clone错误</h4>
<p>在搭建goproxy时，我们通常会在goproxy服务器上手工验证一下是否可以通过git成功拉取私有仓库，如果git clone出现下面错误信息，是什么问题呢？</p>
<pre><code>$ git clone ssh://tonybai@10.10.30.30:29418/go/common
Cloning into 'common'...
Unable to negotiate with 10.10.30.30 port 29418: no matching key exchange method found. Their offer: diffie-hellman-group14-sha1,diffie-hellman-group1-sha1
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
</code></pre>
<p>这里的错误提示信息其实是很清楚明了的。git服务器端支持diffie-hellman-group1-sha1和diffie-hellman-group14-sha1这两种密钥交换方法，而git客户端却默认一个都不支持。</p>
<p>怎么解决呢？我们需要在goproxy所在主机增加一个配置.ssh/config：</p>
<pre><code>// ~/.ssh/config
Host 10.10.30.30
    HostName 10.10.30.30
    User tonybai
    Port 29418
    KexAlgorithms +diffie-hellman-group1-sha1

    IdentityFile ~/.ssh/id_rsa
</code></pre>
<p>有了这条配置后，我们就可以成功clone。</p>
<h4>2. 使用非安全连接</h4>
<p>有些童鞋使用这个方案后会遇到下面问题：</p>
<pre><code>$go get mycompany.com/go/common@latest
go: module mycompany.com/go/common: reading http://10.10.30.30:10000/mycompany.com/go/common/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/common@latest:
    go list -m: mycompany.com/go/common@latest: unrecognized import path "mycompany.com/go/common": https fetch: Get "https://mycompany.com/go/common?go-get=1": dial tcp 127.0.0.1:443: connect: connection refused
</code></pre>
<p>首先，go get得到的服务端响应信息中提示：无法连接127.0.0.1:443，查看goproxy主机的nginx access.log，也无日志。说明goproxy没有发起请求。也就是说问题出在go list命令这块，它为什么要去连127.0.0.1:443？我们的代码服务器使用的可是http而非https方式访问。</p>
<p>这让我想起了Go 1.14中增加的GOINSECURE，go命令默认采用的是secure方式，即https去访问代码仓库的。如果不要求非得以https获取module，或者即便使用https，也不再对server证书进行校验，那么需要设置GOINSECURE环境变量，比如；</p>
<pre><code>export GOINSECURE="mycompany.com"
</code></pre>
<p>这样再获取mycompany.com/&#8230;下面的go module时，就不会出现上面的错误了！</p>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>使用multipart/form-data实现文件的上传与下载</title>
		<link>https://tonybai.com/2021/01/16/upload-and-download-file-using-multipart-form-over-http/</link>
		<comments>https://tonybai.com/2021/01/16/upload-and-download-file-using-multipart-form-over-http/#comments</comments>
		<pubDate>Sat, 16 Jan 2021 03:06:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Form]]></category>
		<category><![CDATA[GET]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[MIME]]></category>
		<category><![CDATA[multipart/form-data]]></category>
		<category><![CDATA[Pipe]]></category>
		<category><![CDATA[POST]]></category>
		<category><![CDATA[request]]></category>
		<category><![CDATA[rfc]]></category>
		<category><![CDATA[upload]]></category>
		<category><![CDATA[URL]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[上传]]></category>
		<category><![CDATA[表单]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3080</guid>
		<description><![CDATA[1. Form简介 Form(中文译为表单)，是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等，还包含被称为控件的特殊元素。用户通常通过修改控件（比如：输入文本、选择菜单项等）来“完成”表单，然后将表单数据以HTTP Get或Post请求的形式提交（submit）给Web服务器。 很多初学者总是混淆HTML和HTTP。其实，http通常作为html传输的承载体，打个比方，html就像乘客，http就像出租车，将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客，很多格式均可作为http这辆出租车的乘客，比如json(over http)、xml(over http)。 在一个HTML文档中，一个表单的标准格式如下： &#60;form action="http://localhost:8080/repositories" method="get"&#62; &#60;input type="text" name="language" value="go" /&#62; &#60;input type="text" name="since" value="monthly" /&#62; &#60;input type="submit" /&#62; &#60;/form&#62; 这样的一个Form被加载到浏览器中后会呈现为一个表单的样式，当在两个文本框中分别输入文本(或以默认的文本作为输入)后，点击“提交(submit)”，浏览器会向http://localhost:8080发出一个HTTP请求，由于Form的method属性为get，因此该HTTP请求会将表单的输入文本作为查询字符串参数(Query String Parameter，在这里即是?language=go&#38;since=monthly)。服务器端处理完该请求后，会返回一个HTTP承载的应答，该应答被浏览器接收后会按特定样式呈现在浏览器窗口中。上述这个过程可以用总结为下面这幅示意图： Form中的method也可以使用post，就像下面这样： &#60;form action="http://localhost:8080/repositories" method="post"&#62; &#60;input type="text" name="language" value="go" /&#62; &#60;input type="text" name="since" value="monthly" /&#62; &#60;input type="submit" /&#62; &#60;/form&#62; 改为post的Form表单在点击提交后发出的http请求与method=get时的请求有何不同呢？不同之处就在于在method=post的情况下，表单的参数不会再以查询字符串参数的形式放在请求的URL中，而是会被写入HTTP的BODY中。我们也将这一过程用一幅示意图的形式总结一下： 由于表单参数被放置在HTTP Body中传输(body中的数据为：language=go&#38;since=monthly)，因此在该HTTP请求的headers中我们会发现新增一个header字段：Content-Type，在这里例子中，它的值为application/x-www-form-urlencoded。我们可以在Form中使用enctype属性改变Form传输数据的内容编码类型，该属性的默认值就是application/x-www-form-urlencoded(即key1=value1&#38;key2=value2&#38;&#8230;的形式)。enctype的其它可选值还包括： text/plain multipart/form-data 采用method=get的Form的表单参数以查询字符串参数的形式放入http请求，这使得其应用场景相对局限，比如： 当参数值很多，参数值很长时，可能会超出URL最大长度限制； 传递敏感数据时，参数值以明文放在HTTP请求头是不安全的； 无法胜任传递二进制数据(比如一个文件内容)的情形。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-0.png" alt="img{512x368}" /></p>
<h3>1. Form简介</h3>
<p><a href="https://www.w3.org/TR/html401/interact/forms.html"><strong>Form</strong>(中文译为表单)</a>，是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等，还包含被称为控件的特殊元素。用户通常通过修改控件（比如：输入文本、选择菜单项等）来“完成”表单，然后将表单数据以HTTP Get或Post请求的形式提交（submit）给Web服务器。</p>
<blockquote>
<p>很多初学者总是混淆HTML和HTTP。其实，http通常作为html传输的承载体，打个比方，html就像乘客，http就像出租车，将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客，很多格式均可作为http这辆出租车的乘客，比如json(over http)、xml(over http)。</p>
</blockquote>
<p>在一个HTML文档中，一个表单的标准格式如下：</p>
<pre><code>&lt;form action="http://localhost:8080/repositories" method="get"&gt;
   &lt;input type="text" name="language" value="go" /&gt;
   &lt;input type="text" name="since" value="monthly" /&gt;
   &lt;input type="submit" /&gt;
&lt;/form&gt;
</code></pre>
<p>这样的一个Form被加载到浏览器中后会呈现为一个表单的样式，当在两个文本框中分别输入文本(或以默认的文本作为输入)后，点击“提交(submit)”，浏览器会向http://localhost:8080发出一个HTTP请求，由于Form的method属性为get，因此该HTTP请求会将表单的输入文本作为查询字符串参数(Query String Parameter，在这里即是<strong>?language=go&amp;since=monthly</strong>)。服务器端处理完该请求后，会返回一个HTTP承载的应答，该应答被浏览器接收后会按特定样式呈现在浏览器窗口中。上述这个过程可以用总结为下面这幅示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-1.png" alt="img{512x368}" /></p>
<p>Form中的method也可以使用post，就像下面这样：</p>
<pre><code>&lt;form action="http://localhost:8080/repositories" method="post"&gt;
   &lt;input type="text" name="language" value="go" /&gt;
   &lt;input type="text" name="since" value="monthly" /&gt;
   &lt;input type="submit" /&gt;
&lt;/form&gt;
</code></pre>
<p>改为post的Form表单在点击提交后发出的http请求与method=get时的请求有何不同呢？不同之处就在于在method=post的情况下，表单的参数不会再以查询字符串参数的形式放在请求的URL中，而是会被写入HTTP的BODY中。我们也将这一过程用一幅示意图的形式总结一下：</p>
<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-2.png" alt="img{512x368}" /></p>
<p>由于表单参数被放置在HTTP Body中传输(body中的数据为：<strong>language=go&amp;since=monthly</strong>)，因此在该HTTP请求的headers中我们会发现新增一个header字段：<strong>Content-Type</strong>，在这里例子中，它的值为<strong>application/x-www-form-urlencoded</strong>。我们可以在Form中使用<strong>enctype</strong>属性改变Form传输数据的内容编码类型，该属性的默认值就是<strong>application/x-www-form-urlencoded</strong>(即key1=value1&amp;key2=value2&amp;&#8230;的形式)。enctype的其它可选值还包括：</p>
<ul>
<li>text/plain</li>
<li>multipart/form-data</li>
</ul>
<p>采用method=get的Form的表单参数以查询字符串参数的形式放入http请求，这使得其应用场景相对局限，比如：</p>
<ul>
<li>当参数值很多，参数值很长时，可能会超出URL最大长度限制；</li>
<li>传递敏感数据时，参数值以明文放在HTTP请求头是不安全的；</li>
<li>无法胜任传递二进制数据(比如一个文件内容)的情形。</li>
</ul>
<p>因此，在面对上述这些情形时，method=post的表单更有优势。当enctype为不同值时，method=post的表单在http Body中传输的数据形式如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-3.png" alt="img{512x368}" /></p>
<p>我们看到：enctype=application/x-www-urlencoded时，Body中的数据呈现为key1=value1&amp;key2=value2&amp;&#8230;的形式，好似URL的查询字符串参数的组合呈现形式；当enctype=text/plain时，这种编码格式也称为raw，即将数据内容原封不动的放入Body中传输，保持数据的原先的编码方式(通常为utf-8)；而当enctype=multipart/form-data时，HTTP Body中的数据以多段(part)的形式呈现，段与段之间使用指定的随机字符串分隔，该随机字符串也会随着HTTP Post请求一并传给服务端(放在Header中的Content-Type的值中，与multipart/form-data使用分号相隔)，如：</p>
<pre><code>Content-Type: multipart/form-data; boundary=--------------------------399501358433894470769897
</code></pre>
<p>我们来看一个稍微复杂些的enctype=multipart/form-data的例子的示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-4.png" alt="img{512x368}" /></p>
<p>我们用Postman模拟了一个包含5个分段(part)的Post请求，其中包含两个文本分段(text)和三个文件分段，并且这三个文件是不同格式的文件，分别是txt，png和json。针对文件分段，Postman使用每个分段中的Content-Type来指明这个分段的数据内容类型。当服务端接收到这些数据时，根据分段Content-Type的指示，便可以有针对性的对分段数据进行解析了。文件分段的默认Content-Type为text/plain；对于无法识别的文件类型（比如：没有扩展名），文件分段的Content-Type通常会设置为<strong>application/octet-stream</strong>。</p>
<p>通过Form上传文件是<a href="https://www.ietf.org/rfc/rfc1867.txt">RFC1867规范</a>赋予html的一种能力，并且该能力已被证明非常有用，并被广泛使用，<strong>甚至</strong>我们可以直接将multipart/form-data作为HTTP Post body的一种数据承载协议在两个端之间传输文件数据。</p>
<h3>2. 支持以multipart/form-data格式上传文件的Go服务器</h3>
<p>http.Request提供了ParseMultipartForm的方法对以multipart/form-data格式传输的数据进行解析，解析即是将数据映射为Request结构的MultipartForm字段的过程：</p>
<pre><code>// $GOROOT/src/net/http/request.go

type Request struct {
    ... ...
    // MultipartForm is the parsed multipart form, including file uploads.
    // This field is only available after ParseMultipartForm is called.
    // The HTTP client ignores MultipartForm and uses Body instead.
    MultipartForm *multipart.Form
    ... ...
}
</code></pre>
<p>multipart.Form代表了一个解析后的multipart/form-data的Body，其结构如下:</p>
<pre><code>// $GOROOT/src/mime/multipart/formdata.go

// Form is a parsed multipart form.
// Its File parts are stored either in memory or on disk,
// and are accessible via the *FileHeader's Open method.
// Its Value parts are stored as strings.
// Both are keyed by field name.
type Form struct {
        Value map[string][]string
        File  map[string][]*FileHeader
}
</code></pre>
<p>我们看到这个Form结构由两个map组成，一个map中存放了所有的value part(就像前面的name、age)，另外一个map存放了所有的file part(就像前面的part1.txt、part2.png和part3.json)。value part集合没什么可说的，map的key就是每个值分段中的”name”； 我们的重点在file part上。每个file part对应一组FileHeader，FileHeader的结构如下：</p>
<pre><code>// $GOROOT/src/mime/multipart/formdata.go
type FileHeader struct {
        Filename string
        Header   textproto.MIMEHeader
        Size     int64

        content []byte
        tmpfile string
}
</code></pre>
<p>每个file part的FileHeader包含五个字段：</p>
<ul>
<li>Filename &#8211; 上传文件的原始文件名</li>
<li>Size &#8211; 上传文件的大小（单位：字节）</li>
<li>content &#8211; 内存中存储的上传文件的（部分或全部）数据内容</li>
<li>tmpfile &#8211; 在服务器本地的临时文件中存储的部分上传文件的数据内容(如果上传的文件大小大于传给ParseMultipartForm的参数maxMemory，剩余部分存储在临时文件中)</li>
<li>Header &#8211; file part的header内容，它亦是一个map，其结构如下：</li>
</ul>
<pre><code>// $GOROOT/src/net/textproto/header.go

// A MIMEHeader represents a MIME-style header mapping
// keys to sets of values.
type MIMEHeader map[string][]string
</code></pre>
<p>我们可以将ParseMultipartForm方法实现的数据映射过程表述为下面这张示意图，这样看起来更为直观：</p>
<p><img src="https://tonybai.com/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-5.png" alt="img{512x368}" /></p>
<p>有了上述对通过multipart/form-data格式上传文件的原理的拆解，我们就可以很容易地利用Go http包实现一个简单的支持以multipart/form-data格式上传文件的Go服务器：</p>
<pre><code>// github.com/bigwhite/experiments/multipart-formdata/server/file_server1.go
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

const uploadPath = "./upload"

func handleUploadFile(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(100)
    mForm := r.MultipartForm

    for k, _ := range mForm.File {
        // k is the key of file part
        file, fileHeader, err := r.FormFile(k)
        if err != nil {
            fmt.Println("inovke FormFile error:", err)
            return
        }
        defer file.Close()
        fmt.Printf("the uploaded file: name[%s], size[%d], header[%#v]\n",
            fileHeader.Filename, fileHeader.Size, fileHeader.Header)

        // store uploaded file into local path
        localFileName := uploadPath + "/" + fileHeader.Filename
        out, err := os.Create(localFileName)
        if err != nil {
            fmt.Printf("failed to open the file %s for writing", localFileName)
            return
        }
        defer out.Close()
        _, err = io.Copy(out, file)
        if err != nil {
            fmt.Printf("copy file err:%s\n", err)
            return
        }
        fmt.Printf("file %s uploaded ok\n", fileHeader.Filename)
    }
}

func main() {
    http.HandleFunc("/upload", handleUploadFile)
    http.ListenAndServe(":8080", nil)
}
</code></pre>
<p>我们可以用Postman或下面curl命令向上述文件服务器同时上传两个文件part1.txt和part3.json：</p>
<pre><code>curl --location --request POST ':8080/upload' \
--form 'name="tony bai"' \
--form 'age="23"' \
--form 'file1=@"/your_local_path/part1.txt"' \
--form 'file3=@"/your_local_path/part3.json"'
</code></pre>
<p>文件上传服务器的运行输出日志如下：</p>
<pre><code>$go run file_server1.go
the uploaded file: name[part3.json], size[130], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file3\"; filename=\"part3.json\""}, "Content-Type":[]string{"application/json"}}]
file part3.json uploaded ok
the uploaded file: name[part1.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"part1.txt\""}, "Content-Type":[]string{"text/plain"}}]
file part1.txt uploaded ok
</code></pre>
<p>之后我们可以看到：文件上传服务器成功地将接收到的part1.txt和part3.json存储到了当前路径下的upload目录中了！</p>
<h3>3. 支持以multipart/form-data格式上传文件的Go客户端</h3>
<p>前面进行文件上传的客户端要么是浏览器，要么是Postman，要么是curl，如果我们自己构要造一个支持以multipart/form-data格式上传文件的客户端，应该如何做呢？我们需要按照multipart/form-data的格式构造HTTP请求的包体(Body)，还好通过Go标准库提供的mime/multipart包，我们可以很容易地构建出满足要求的包体：</p>
<pre><code>// github.com/bigwhite/experiments/multipart-formdata/client/client1.go

... ...
var (
    filePath string
    addr     string
)

func init() {
    flag.StringVar(&amp;filePath, "file", "", "the file to upload")
    flag.StringVar(&amp;addr, "addr", "localhost:8080", "the addr of file server")
    flag.Parse()
}

func main() {
    if filePath == "" {
        fmt.Println("file must not be empty")
        return
    }

    err := doUpload(addr, filePath)
    if err != nil {
        fmt.Printf("upload file [%s] error: %s", filePath, err)
        return
    }
    fmt.Printf("upload file [%s] ok\n", filePath)
}

func createReqBody(filePath string) (string, io.Reader, error) {
    var err error

    buf := new(bytes.Buffer)
    bw := multipart.NewWriter(buf) // body writer

    f, err := os.Open(filePath)
    if err != nil {
        return "", nil, err
    }
    defer f.Close()

    // text part1
    p1w, _ := bw.CreateFormField("name")
    p1w.Write([]byte("Tony Bai"))

    // text part2
    p2w, _ := bw.CreateFormField("age")
    p2w.Write([]byte("15"))

    // file part1
    _, fileName := filepath.Split(filePath)
    fw1, _ := bw.CreateFormFile("file1", fileName)
    io.Copy(fw1, f)

    bw.Close() //write the tail boundry
    return bw.FormDataContentType(), buf, nil
}

func doUpload(addr, filePath string) error {
    // create body
    contType, reader, err := createReqBody(filePath)
    if err != nil {
        return err
    }

    url := fmt.Sprintf("http://%s/upload", addr)
    req, err := http.NewRequest("POST", url, reader)

    // add headers
    req.Header.Add("Content-Type", contType)

    client := &amp;http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("request send error:", err)
        return err
    }
    resp.Body.Close()
    return nil
}
</code></pre>
<p>显然上面这个client端的代码的核心是createReqBody函数：</p>
<ul>
<li>该client在body中创建了三个分段，前两个分段仅仅是我为了演示如何创建text part而故意加入的，真正的上传文件客户端是不需要创建这两个分段(part)的；</li>
<li>createReqBody使用bytes.Buffer作为http body的临时存储；</li>
<li>构建完body内容后，不要忘记调用multipart.Writer的Close方法以写入结尾的boundary标记。</li>
</ul>
<p>我们使用这个客户端向前面的支持以multipart/form-data格式上传文件的服务器上传一个文件：</p>
<pre><code>// 客户端
$go run client1.go -file hello.txt
upload file [hello.txt] ok

// 服务端
$go run file_server1.go

http request: http.Request{Method:"POST", URL:(*url.URL)(0xc00016e100), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Length":[]string{"492"}, "Content-Type":[]string{"multipart/form-data; boundary=b55090594eaa1aaac1abad1d89a77ae689130d79d6f66af82590036bd8ba"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000146380), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:492, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc000110d50), Trailer:http.Header(nil), RemoteAddr:"[::1]:58569", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(&lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0001463c0)}
the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"application/octet-stream"}}]
file hello.txt uploaded ok
</code></pre>
<p>我们看到hello.txt这个文本文件被成功上传！</p>
<h3>4. 自定义file分段中的header</h3>
<p>从上面file_server1的输出来看，client1这个客户端上传文件时在file分段(part)中设置的Content-Type为默认的<strong>application/octet-stream</strong>。有时候，服务端可能会需要根据这个Content-Type做分类处理，需要客户端给出准确的值。上面的client1实现中，我们使用了multipart.Writer.CreateFormFile这个方法来创建file part：</p>
<pre><code>// file part1
_, fileName := filepath.Split(filePath)
fw1, _ := bw.CreateFormFile("file1", fileName)
io.Copy(fw1, f)
</code></pre>
<p>下面是标准库中CreateFormFile方法的实现代码：</p>
<pre><code>// $GOROOT/mime/multipart/writer.go
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
        h := make(textproto.MIMEHeader)
        h.Set("Content-Disposition",
                fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
                        escapeQuotes(fieldname), escapeQuotes(filename)))
        h.Set("Content-Type", "application/octet-stream")
        return w.CreatePart(h)
}
</code></pre>
<p>我们看到无论待上传的文件是什么类型，CreateFormFile均将Content-Type置为application/octet-stream这一默认值。如果我们要自定义file part中Header字段Content-Type的值，我们就不能直接使用CreateFormFile，不过我们可以参考其实现：</p>
<pre><code>// github.com/bigwhite/experiments/multipart-formdata/client/client2.go

var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

func escapeQuotes(s string) string {
    return quoteEscaper.Replace(s)
}

func createReqBody(filePath string) (string, io.Reader, error) {
    var err error

    buf := new(bytes.Buffer)
    bw := multipart.NewWriter(buf) // body writer

    f, err := os.Open(filePath)
    if err != nil {
        return "", nil, err
    }
    defer f.Close()

    // text part1
    p1w, _ := bw.CreateFormField("name")
    p1w.Write([]byte("Tony Bai"))

    // text part2
    p2w, _ := bw.CreateFormField("age")
    p2w.Write([]byte("15"))

    // file part1
    _, fileName := filepath.Split(filePath)
    h := make(textproto.MIMEHeader)
    h.Set("Content-Disposition",
        fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
            escapeQuotes("file1"), escapeQuotes(fileName)))
    h.Set("Content-Type", "text/plain")
    fw1, _ := bw.CreatePart(h)
    io.Copy(fw1, f)

    bw.Close() //write the tail boundry
    return bw.FormDataContentType(), buf, nil
}
</code></pre>
<p>我们通过textproto.MIMEHeader实例来自定义file part的header部分，然后基于该实例调用CreatePart创建file part，之后将hello.txt的文件内容写到该part的header后面。</p>
<p>我们运行client2来上传hello.txt文件，在file_server侧，我们就能看到如下日志：</p>
<pre><code>the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"text/plain"}}]
file hello.txt uploaded ok
</code></pre>
<p>我们看到file part的Content-Type的值已经变为我们设定的<strong>text/plain</strong>了。</p>
<h3>5. 解决上传大文件的问题</h3>
<p>在上面的客户端中存在一个问题，那就是我们在构建http body的时候，使用了一个bytes.Buffer加载了待上传文件的所有内容，这样一来，如果待上传的文件很大的话，内存空间消耗势必过大。那么如何将每次上传内存文件时对内存的使用限制在一个适当的范围，或者说上传文件所消耗的内存空间不因待传文件的变大而变大呢？我们来看下面的这个解决方案：</p>
<pre><code>// github.com/bigwhite/experiments/multipart-formdata/client/client3.go
... ...
func createReqBody(filePath string) (string, io.Reader, error) {
    var err error
    pr, pw := io.Pipe()
    bw := multipart.NewWriter(pw) // body writer
    f, err := os.Open(filePath)
    if err != nil {
        return "", nil, err
    }

    go func() {
        defer f.Close()
        // text part1
        p1w, _ := bw.CreateFormField("name")
        p1w.Write([]byte("Tony Bai"))

        // text part2
        p2w, _ := bw.CreateFormField("age")
        p2w.Write([]byte("15"))

        // file part1
        _, fileName := filepath.Split(filePath)
        h := make(textproto.MIMEHeader)
        h.Set("Content-Disposition",
            fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
                escapeQuotes("file1"), escapeQuotes(fileName)))
        h.Set("Content-Type", "application/pdf")
        fw1, _ := bw.CreatePart(h)
        cnt, _ := io.Copy(fw1, f)
        log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)
        bw.Close() //write the tail boundry
        pw.Close()
    }()
    return bw.FormDataContentType(), pr, nil
}

func doUpload(addr, filePath string) error {
    // create body
    contType, reader, err := createReqBody(filePath)
    if err != nil {
        return err
    }

    log.Printf("createReqBody ok\n")
    url := fmt.Sprintf("http://%s/upload", addr)
    req, err := http.NewRequest("POST", url, reader)

    //add headers
    req.Header.Add("Content-Type", contType)

    client := &amp;http.Client{}
    log.Printf("upload %s...\n", filePath)
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("request send error:", err)
        return err
    }
    resp.Body.Close()
    log.Printf("upload %s ok\n", filePath)
    return nil
}
</code></pre>
<p>在这个方案中，我们通过io.Pipe函数创建了一个读写管道，其写端作为io.Writer实例传给multipart.NewWriter，读端返回给调用者，用于构建http request时使用。io.Pipe基于channel实现，其内部不维护任何内存缓存：</p>
<pre><code>// $GOROOT/src/io/pipe.go
func Pipe() (*PipeReader, *PipeWriter) {
        p := &amp;pipe{
                wrCh: make(chan []byte),
                rdCh: make(chan int),
                done: make(chan struct{}),
        }
        return &amp;PipeReader{p}, &amp;PipeWriter{p}
}
</code></pre>
<p>通过Pipe返回的读端读取管道中数据时，如果尚未有数据写入管道，那么读端会像读取channel那样阻塞在那里。由于http request在被发送时(client.Do(req))才会真正基于构建req时传入的reader对Body数据进行读取，因此client会阻塞在对管道的read上。显然我们不能将读写两端的操作放在一个goroutine中，那样会因所有goroutine都挂起而导致panic。在上面的client3.go代码中，函数createReqBody内部创建了一个新goroutine，将真正构建multipart/form-data body的工作放在了新goroutine中。新goroutine最终会将待上传文件的数据通过管道写端写入管道：</p>
<pre><code>cnt, _ := io.Copy(fw1, f)
</code></pre>
<p>而这些数据也会被client读取并通过网络连接传输出去。io.Copy的实现如下：</p>
<pre><code>// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader) (written int64, err error) {
        return copyBuffer(dst, src, nil)
}
</code></pre>
<p>io.copyBuffer内部维护了一个默认32k的小buffer，它每次从src尝试最大读取32k的数据，并写入到dst中，直到读完为止。这样无论待上传的文件有多大，我们实际上每次上传所分配的内存仅有32k。</p>
<p>下面就是我们用client3.go上传一个大小为252M的文件的日志：</p>
<pre><code>$go run client3.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
2021/01/10 12:56:45 createReqBody ok
2021/01/10 12:56:45 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
2021/01/10 12:56:46 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
2021/01/10 12:56:46 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok

$go run file_server1.go
http request: http.Request{Method:"POST", URL:(*url.URL)(0xc000078200), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Type":[]string{"multipart/form-data; boundary=4470ba3867218f1130878713da88b5bd79f33dfbed65566e4fd76a1ae58d"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000026240), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:-1, TransferEncoding:[]string{"chunked"}, Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc0000122a0), Trailer:http.Header(nil), RemoteAddr:"[::1]:54899", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(&lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc000026280)}
the uploaded file: name[ICME-2019-Tutorial-final.pdf], size[264517032], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"ICME-2019-Tutorial-final.pdf\""}, "Content-Type":[]string{"application/pdf"}}]
file ICME-2019-Tutorial-final.pdf uploaded ok

$ls -l upload
-rw-r--r--  1 tonybai  staff  264517032  1 14 12:56 ICME-2019-Tutorial-final.pdf
</code></pre>
<p>如果你觉得32k仍然很大，每次上传要使用更小的buffer，你可以用io.CopyBuffer替代io.Copy：</p>
<pre><code>// github.com/bigwhite/experiments/multipart-formdata/client/client4.go

func createReqBody(filePath string) (string, io.Reader, error) {
    var err error
    pr, pw := io.Pipe()
    bw := multipart.NewWriter(pw) // body writer
    f, err := os.Open(filePath)
    if err != nil {
        return "", nil, err
    }

    go func() {
        defer f.Close()
        // text part1
        p1w, _ := bw.CreateFormField("name")
        p1w.Write([]byte("Tony Bai"))

        // text part2
        p2w, _ := bw.CreateFormField("age")
        p2w.Write([]byte("15"))

        // file part1
        _, fileName := filepath.Split(filePath)
        h := make(textproto.MIMEHeader)
        h.Set("Content-Disposition",
            fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
                escapeQuotes("file1"), escapeQuotes(fileName)))
        h.Set("Content-Type", "application/pdf")
        fw1, _ := bw.CreatePart(h)
        var buf = make([]byte, 1024)
        cnt, _ := io.CopyBuffer(fw1, f, buf)
        log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)
        bw.Close() //write the tail boundry
        pw.Close()
    }()
    return bw.FormDataContentType(), pr, nil
}
</code></pre>
<p>运行这个client4：</p>
<pre><code>$go run client4.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
2021/01/10 13:39:06 createReqBody ok
2021/01/10 13:39:06 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
2021/01/10 13:39:09 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
2021/01/10 13:39:09 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok
</code></pre>
<p>你会看到虽然上传成功了，但由于每次read仅能读1k数据，对于大文件来说，其上传的时间消耗增加了不少。</p>
<h3>6. 下载文件</h3>
<p>客户端基于multipart/form-data下载文件的过程的原理与上面的file_server1接收客户端上传文件的原理是一样的，这里就将这个功能的Go实现作为“作业”留给各位读者了:)。</p>
<h3>7. 参考资料</h3>
<ul>
<li><a href="https://www.ietf.org/rfc/rfc1867.txt">Form-based File Upload in HTML</a></li>
<li><a href="https://www.ietf.org/rfc/rfc2388.txt">Returning Values from Forms: multipart/form-data</a></li>
<li><a href="https://book.douban.com/subject/27204133/">《Go Web Programming》</a></li>
<li><a href="https://www.ietf.org/rfc/rfc7233.txt">Hypertext Transfer Protocol (HTTP/1.1): Range Requests</a></li>
</ul>
<p>本文中涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/multipart-formdata">这里</a>(https://github.com/bigwhite/experiments/tree/master/multipart-formdata)下载。</p>
<hr />
<p>“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！</p>
<p><img src="http://image.tonybai.com/img/202011/go-column-pgo-with-qr-and-text.png" alt="" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/01/16/upload-and-download-file-using-multipart-form-over-http/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
