You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Scala中运行Ruby脚本或复刻ActiveSupport加密逻辑?

问题

我要做一个Scala Web API,功能是接收用户输入的SECRET、ID、URL,生成加密后的字符串,替代原来的Ruby脚本。试了两种方法都卡壳:

  1. 想用org.jruby在Scala里直接跑Ruby脚本,但build.sbt里的依赖配置一直出问题;
  2. 尝试把Ruby脚本逻辑转成Scala代码,但是搞不定ActiveSupport::MessageEncryptor的encrypt_and_sign加密逻辑。

原Ruby脚本如下:

# Usage: ./gen_password.rb SECRET ID URL
# Example: ./gen_password.rb 73191239133f 1234567 myshop.com
# Note: 73191239133f is a fake secret. It will not work.

require "active_support/all"

SECRET = ARGV[0]
ID = ARGV[1]
URL = ARGV[2]

encryptor = ActiveSupport::MessageEncryptor.new(SECRET)

puts("#{ID}+#{URL}+#{Time.now.utc.to_i}")
puts encryptor.encrypt_and_sign("#{ID}+#{URL}+#{Time.now.utc.to_i}")
解决方案

方案一:搞定JRuby依赖,直接在Scala里跑Ruby脚本

先把build.sbt的依赖配对,再用JRuby的API调用脚本:

  1. build.sbt添加正确依赖:
libraryDependencies ++= Seq(
  "org.jruby" % "jruby-core" % "9.4.5.0",
  "org.jruby" % "jruby-stdlib" % "9.4.5.0"
)
  1. Scala调用代码示例:
import org.jruby.embed.ScriptingContainer

object PasswordGenerator {
  def generate(secret: String, id: String, url: String): String = {
    val jruby = new ScriptingContainer()
    // 把参数传给Ruby环境
    jruby.put("SECRET", secret)
    jruby.put("ID", id)
    jruby.put("URL", url)
    
    // 直接内嵌Ruby逻辑,不用读外部脚本文件
    val rubyScript = """
      require "active_support/all"
      encryptor = ActiveSupport::MessageEncryptor.new(SECRET)
      plaintext = "#{ID}+#{URL}+#{Time.now.utc.to_i}"
      encryptor.encrypt_and_sign(plaintext)
    """.stripMargin
    
    // 执行脚本并返回结果
    jruby.runScriptlet(rubyScript).asInstanceOf[String]
  }
}

首次运行会自动下载ActiveSupport的gem,确保网络正常就行;要是gem加载有问题,可以提前用jruby -S gem install activesupport装好。

方案二:自己写Scala代码复刻加密逻辑

ActiveSupport::MessageEncryptor#encrypt_and_sign的核心步骤是:序列化明文→生成随机IV→AES加密→HMAC签名→拼接后Base64编码。以下是对应实现:

1. 先加必要依赖(build.sbt)

libraryDependencies ++= Seq(
  "commons-codec" % "commons-codec" % "1.15",
  "org.bouncycastle" % "bcprov-jdk15on" % "1.70" // 用来处理Ruby Marshal序列化
)

2. Scala实现代码

import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.{Cipher, Mac}
import javax.crypto.spec.{IvParameterSpec, PBEKeySpec, SecretKeySpec}
import scala.util.Random

object ActiveSupportEncryptor {
  private val AES_ALG = "AES/CBC/PKCS5Padding"
  private val HMAC_ALG = "HmacSHA256"
  private val KEY_ITERATIONS = 1000
  private val SALT = "encrypted cookie".getBytes(StandardCharsets.UTF_8)
  private val AES_KEY_LEN = 32 // AES-256
  private val HMAC_KEY_LEN = 32

  // 模拟ActiveSupport的密钥派生逻辑,生成AES加密密钥和HMAC签名密钥
  private def getKeys(secret: String): (SecretKeySpec, SecretKeySpec) = {
    val keyFactory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
    val spec = new PBEKeySpec(secret.toCharArray, SALT, KEY_ITERATIONS, (AES_KEY_LEN + HMAC_KEY_LEN) * 8)
    val keyBytes = keyFactory.generateSecret(spec).getEncoded
    
    val aesKey = new SecretKeySpec(keyBytes.take(AES_KEY_LEN), "AES")
    val hmacKey = new SecretKeySpec(keyBytes.takeRight(HMAC_KEY_LEN), HMAC_ALG)
    (aesKey, hmacKey)
  }

  // 简化版Ruby Marshal序列化,只支持字符串(复杂对象需要完整实现Marshal格式)
  private def marshal(str: String): Array[Byte] = {
    val strBytes = str.getBytes(StandardCharsets.UTF_8)
    // Ruby Marshal字符串的格式:\x04\x08I"[长度][字符串]"\x06:\x06ET
    Array(0x04.toByte, 0x08.toByte, 'I'.toByte, '"'.toByte) ++ 
    Array((strBytes.length & 0xFF).toByte) ++ 
    strBytes ++ 
    Array('"'.toByte, 0x06.toByte, ':'.toByte, 0x06.toByte, 'E'.toByte, 'T'.toByte)
  }

  def encryptAndSign(secret: String, plaintext: String): String = {
    val (aesKey, hmacKey) = getKeys(secret)
    
    // 生成16字节随机IV(AES块大小)
    val iv = new Array[Byte](16)
    Random.nextBytes(iv)
    
    // AES加密序列化后的明文
    val cipher = Cipher.getInstance(AES_ALG)
    cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv))
    val encrypted = cipher.doFinal(marshal(plaintext))
    
    // 生成HMAC签名,签名内容是IV+加密数据
    val mac = Mac.getInstance(HMAC_ALG)
    mac.init(hmacKey)
    val signature = mac.doFinal(iv ++ encrypted)
    
    // 拼接IV、加密数据、签名,Base64编码
    Base64.getEncoder.encodeToString(iv ++ encrypted ++ signature)
  }

  // 测试用例
  def main(args: Array[String]): Unit = {
    val secret = "73191239133f"
    val id = "1234567"
    val url = "myshop.com"
    val plaintext = s"$id+$url+${System.currentTimeMillis() / 1000}"
    println(plaintext)
    println(encryptAndSign(secret, plaintext))
  }
}

注意:

  • 这个Marshal序列化是简化版,只处理字符串;如果要兼容Ruby里的复杂对象,得完整实现Ruby Marshal的格式解析;
  • 可以拿Ruby脚本生成的结果和Scala代码的结果对比,确认逻辑一致;
  • 如果你的ActiveSupport用了其他加密算法(比如AES-GCM),需要调整代码里的算法参数。

内容的提问来源于stack exchange,提问作者Rudrani Angira

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 15:15:15