Play Framework Crypto 迁移指南:从 Crypto 工具类到安全的现代密码学实践
2026/9/23 11:26:16 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】playframework

The Community Maintained High Velocity Web Framework For Java and Scala.

项目地址:https://gitcode.com/gh_mirrors/pl/playframework
点击查看免费下载

本文是 Play Framework 2.5 系列迁移文档的核心章节,系统讲解旧版Crypto工具对象被废弃的原因、其背后消息认证(MAC)与对称加密(AES)设计上的安全隐患,以及迁移到 Kalium、Tink 或纯 JCA 的三条推荐路径。读完本文,你将理解 Play 为何将密码学能力拆分为CookieSignerCSRFTokenSigner等专用 trait,并能为自己的应用选择安全、合适的替代实现。

背景:从Crypto单例到专用 Signer Trait

从 Play 1.x 起,Play 框架就内置了一个名为Crypto的对象,提供若干密码学操作,并被 Play 内部使用。虽然该对象从未在官方文档中被正式介绍,但它的 scaladoc 中将其描述为"密码学工具"(cryptographic utilities),并附带了一段重要警告:

"这些工具旨在提供便利,但使用该类之前,务必阅读每个方法的文档并理解加密背后的概念。安全加密是困难的,任何东西都无法替代对密码学的充分理解。这些方法并不适合所有加密需求。"

然而,经过实践检验,"以便利工具的形式提供密码学能力"这一思路被证明并不可行。因此从2.5.x开始,Play 将原本属于Crypto的框架专用功能拆分成了三个独立的 trait,并正式将Crypto单例对象标记为deprecated(废弃)

新组件职责
CookieSigner为 Cookie(尤其是 Session Cookie)提供消息认证码(MAC)签名
CSRFTokenSigner生成与校验 CSRF 令牌,防止 BREACH 攻击
AESSigner对称加密相关的签名辅助(AES 相关能力)

从当前仓库源码可以验证这一拆分结果:CookieSignerCSRFTokenSigner分别定义在 core/play/src/main/scala/play/api/libs/crypto/CookieSigner.scala 与 core/play/src/main/scala/play/api/libs/crypto/CSRFTokenSigner.scala,并提供对应的 Java 接口版本 core/play/src/main/java/play/libs/crypto/CookieSigner.java。依赖注入时,可通过 CryptoComponents 获取cookieSigner()csrfTokenSigner()两个组件。

关联阅读:Play 2.5 总迁移指南 Migration25.md 中" Crypto Deprecated"一节也明确指出:加密迁移方案取决于你的使用场景,尤其是是否涉及不安全的密码学原语构造,推荐顺序是"能上 Kalium 就上 Kalium,否则用 Tink 或直接用 JCA"。

消息认证(Message Authentication):为什么不应滥用Crypto.sign

Play 使用Crypto.sign方法为 Session Cookie 提供消息认证。该方法在内部用于对 Session Cookie 进行签名,但官方明确建议:不应在签名 Cookie 之外的场景使用Crypto.sign,理由有以下三点。

MAC 算法独立性:Play 需要保留升级 HMAC 的空间

Play 当前使用HMAC-SHA1对 Session Cookie 进行签名与校验。HMAC(基于哈希的消息认证码)是一种利用密钥(即play.crypto.secret配置的应用密钥)配合消息摘要函数,认证数据未被篡改的密码学函数,此处采用的摘要函数是 SHA-1。

虽然 SHA-1 近年遭受了若干攻击,但当它与 HMAC 配合用于"消息真实性"验证时,仍然是安全的。关键在于:Play 需要保留"按需切换到其他 HMAC 函数"的灵活性,因此 HMAC 的具体选择不应成为公共 API 的一部分。如果Crypto.sign被大量外部代码直接依赖,Play 内部升级 HMAC 算法的自由度就会受限,这正是它不应作为公共 API 的理由之一。

从仓库源码看,当前DefaultCookieSigner的默认实现确实仍固定使用"HmacSHA1"算法(见 CookieSigner.scala),并通过Mac.getInstance(HmacSHA1)获取 JCE 提供的 MAC 实例,最后用Codecs.toHexString输出十六进制签名字符串:

def sign(message: String): String = { sign(message, secretConfiguration.secret.getBytes(StandardCharsets.UTF_8)) }

其 Scala 与 Java 接口均提供了两个重载:一个使用应用密钥签名,一个显式传入自定义key字节数组签名。单元测试 CookieSignerSpec.scala 验证了这两种签名路径——例如使用密钥"0123456789abcdef"对文本"Play Framework 2.0"签名,得到的 HMAC-SHA1 十六进制摘要为94f63b1470ee74e15dc15fd704e26b0df36ef848

潜在的新功能需求:Session 缺少过期时间

Play 目前只对 Session Cookie 进行签名,但没有为其附加任何会话超时或过期时间。这意味着,只要攻击者能接触到请求(active attacker),就可以把一个 Session Cookie 替换成另一个 Session Cookie(cookie swapping)。要解决这类问题,Play 可能需要给 Session 引入时间戳等额外机制——而这类演进同样要求签名逻辑不能过早固化到公共 API 中。

误用为密码哈希:MAC 不是密码存储方案

请勿将Crypto.sign或任何形式的 HMAC 用于密码哈希!这是文档中特别强调的告诫。原因是两者的设计目标截然相反:

  • MAC 追求快与廉价:签名/验签需要高频执行,设计上要求计算开销小;
  • 密码哈希追求慢与昂贵:为了抵抗暴力破解与 GPU 加速攻击,密码哈希需要刻意引入计算成本。

因此正确的密码存储应使用scrypt、bcrypt 或 PBKDF2等专用算法。在 Java 生态中,jBCrypt是最广为人知的 bcrypt 实现之一。

对称加密(Symmetric Encryption):Crypto.encryptAES的安全隐患

Crypto还包含两个对称加密方法:Crypto.encryptAESCrypto.decryptAES。这两个方法从未被 Play 内部使用,但社区投入了大量精力对其安全性进行审查。这两个方法同样将被废弃,并可能在未来的版本中移除。

与前面的警告一致,这两个方法"并非普遍安全"——存在若干常见的工作模式(mode of operation)在使用它们时是不安全的。下面逐一分析Crypto.encryptAES存在的密码学问题。

再次强调:Crypto.encryptAES从未在 Play 内部被直接使用,因此这不是Play 自身的安全漏洞。

无认证的流密码:AES-CTR 的可塑性(Malleability)

Crypto.encryptAES默认使用AES-CTR模式。CTR 模式属于流密码类工作模式,它只提供加密(confidentiality),不提供认证(authentication)——也就是说,活跃攻击者可以把密文内容替换成其他内容而不被发现。这种"密文可被篡改"的性质被称为可塑性(malleability),在特定条件下甚至可能让攻击者恢复出明文,或任意改写消息内容。

业界有两种缓解该问题的标准构造:

  1. Encrypt Then MAC(先加密后签名):对密文再施加一个 MAC;
  2. 认证加密(Authenticated Encryption):直接使用同时提供认证与加密的算法,例如AES-GCM

Play 内部用Crypto.encryptAES加密 Session Cookie 的内容(该内容本身已附加了 MAC)。由于 Play 采用的是Encrypt Then MAC构造,因此它并不易受此类攻击——但那些在没有 MAC 保护的情况下使用对称加密的用户,则存在潜在风险。

文档同时建议:不要自行实现 Encrypt-Then-MAC 构造,正确做法是直接使用认证加密,让认证与加密在同一算法中完成(详见下文迁移章节)。

违反密钥分离原则(Key Separation Principle)

虽然"AES + HMAC"被认为是安全构造,但 Play 使用密钥的方式存在一个设计问题:密钥分离原则要求一把密钥只服务于一个目的。而在Crypto时代,play.crypto.secret不仅用于签名,还被Crypto.encryptAES用于加密——同一把密钥被复用于两个不同用途。

文档引用密码学社区的分析指出:对于"HMAC vs AES"这一组合,目前没有已知的密钥混用干扰,密码学家的普遍共识是 AES 与 SHA-1/SHA-256 "足够不同",同一密钥同时用于 AES 与 HMAC 不应产生实际问题。因此对于正在使用Crypto.encryptAES的用户,密钥混用并不会立刻带来安全漏洞。

但随着应用规模增大,密钥分离原则可能在另一个维度被违反:如果Crypto.encryptAES被用于多种不同用途,同样建议为不同用途分配独立密钥

工作模式的全局配置问题

如前所述,AES 支持多种工作模式,而不同模式可能需要不同的附加安全措施。Play 却通过play.crypto.aes.transformation这一配置项提供全局的工作模式配置——该配置会影响整个应用,包括所有使用Crypto.encryptAES的第三方库。这样一来,改动这一配置项对整个应用的实际影响范围很难预估,这也成为其被废弃的另一个理由。

迁移路径(Migration):从Crypto平滑过渡

Crypto功能迁移有以下几条路径,按推荐优先级排序:Kalium、Tink、纯 JCA

Kalium(首选):基于 libsodium 的高级密码学库

如果你的生产环境可以控制二进制依赖,且没有必须使用 NIST 批准算法的外部要求,推荐使用Kalium——它是 libsodium 库的封装。

  • 需要Crypto.sign的 MAC 替代品:使用org.abstractj.kalium.keys.AuthenticationKey,它实现了HMAC-SHA512/256
  • 需要Crypto.encryptAES的对称加密替代品:使用org.abstractj.kalium.crypto.SecretBox,它实现了secret-key authenticated encryption(密钥认证加密),天然同时提供加密与认证。

注意:Kalium 要求环境中安装 libsodium 二进制库,官方建议安装经你亲自验证过的源码构建版本

Tink:纯 Java 且支持 NIST 批准算法的选择

如果你需要纯 Java 方案,或必须依赖 NIST 批准的算法,可以使用Tink——一个构建在 JCA 之上的高层密码学库。文档特别说明:Tink 的成熟度与支持力度不如 libsodium / Kalium,因此Kalium 仍然优先

  • 需要Crypto.sign的 MAC 替代品:使用com.google.crypto.tink.mac.MacKeyTemplates
  • 需要Crypto.encryptAES的对称加密替代品:使用com.google.crypto.tink.aead.AeadKeyTemplates

JCA:保持原算法不变的最低改动方案

Kalium 与 Tink 使用的密码学原语都与Crypto不同。如果希望在不改变底层算法(如继续使用 AES 与 HMAC)的前提下完成迁移,最合适的做法是把 Crypto 库中的相关代码抽取到用户自己的类中,由应用层自行维护,再按安全最佳实践(如使用认证加密、隔离密钥用途)加以改造。

迁移决策速查

你的需求推荐方案
替代Crypto.sign(MAC)且环境可控KaliumAuthenticationKey(HMAC-SHA512/256)
替代Crypto.sign(MAC)且需纯 Java/NISTTinkMacKeyTemplates
替代Crypto.encryptAES(认证加密)且环境可控KaliumSecretBox
替代Crypto.encryptAES(认证加密)且需纯 Java/NISTTinkAeadKeyTemplates
必须保持 AES/HMAC 原算法不变抽取 Crypto 源码为用户级类,自行加固

当前仓库中的替代实现:Signer 是如何工作的

迁移到 2.5 之后,Play 自身的 Session Cookie 签名链路已经完整建立在CookieSigner之上,可以在仓库源码中直接印证:

  • Session.scala 中,DefaultSessionCookieBakerLegacySessionCookieBaker均通过构造器注入CookieSigner,其中LegacySessionCookieBaker的注释明确写着"以 Play 2.5.x 风格签名 Session Cookie",cookieSigner参数标注为"通常是 HMAC-SHA1";
  • CookieSignerProvider(见 CookieSigner.scala)从SecretConfiguration构建DefaultCookieSigner,而SecretConfiguration定义在 HttpConfiguration.scala,其默认密钥为"changeme",并支持通过配置指定 JCE Provider;
  • 密钥与 Provider 的配置项为play.http.secret.provider(旧配置名play.crypto.provider仍可通过 deprecated 机制读取,见 HttpConfiguration.scala),DefaultCookieSigner在实例化 MAC 时若指定了 Provider 则优先使用Mac.getInstance(HmacSHA1, p),否则使用平台默认 JCE Provider。

CSRFTokenSigner 的防 BREACH 设计

CSRFTokenSigner的默认实现(CSRFTokenSigner.scala)值得专门一提,它展示了 Play 在"签名"场景下的实战密码学设计:

  • signToken会将当前时间戳作为 nonce 与原始 token 拼接后签名,输出格式为签名-时间戳-原始token。其核心目的正如源码注释所述:"主要为了抵御BREACH 漏洞——在不改变 token 实际值的前提下,让 token 每次请求看起来都是随机的";
  • extractSignedToken-分割并校验签名,校验时使用MessageDigest.isEqual进行常数时间比较,避免时序侧信道攻击;
  • generateToken使用SecureRandom生成 12 字节随机数并转为十六进制;默认实现还会在初始化时调用random.nextBytes(new ArrayByte)进行 440 位的自播种(对应 NIST SP800-90A 建议),以兼容旧版 Windows 上 SHA1PRNG 的弱自播种问题。

这些实现细节表明:即使是"为 Cookie 签名"这一看似简单的需求,Play 也内建了对抗活跃攻击者(nonce 化、常数时间比较)与熵不足(SecureRandom 自播种)的多重防护——这正是"密码学 API 应由框架谨慎设计、而非暴露给用户随意拼装"这一迁移主旨的最佳注解。

进一步阅读

密码学 API 的设计远比表面看起来复杂。文档推荐了以下关于密码学设计与 API 复杂性的公开资料,供深入研究:

  • The Long Journey from Papers to Software: Crypto APIs
  • What's Wrong with Crypto API Design
  • Real World Crypto 2015: Error-prone cryptographic designs (djb)及其配套幻灯片

此外,OWASP 的Cryptographic Storage Cheatsheet是学习安全加密存储实践的重要参考资料。理解这些资料,有助于在迁移到 Kalium / Tink / JCA 时做出正确的算法选择与构造决策。

  • 后端
  • Web框架

【免费下载链接】playframework

The Community Maintained High Velocity Web Framework For Java and Scala.

项目地址:https://gitcode.com/gh_mirrors/pl/playframework
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询