如何在Scala中运行Ruby脚本或复刻ActiveSupport加密逻辑?
问题
我要做一个Scala Web API,功能是接收用户输入的SECRET、ID、URL,生成加密后的字符串,替代原来的Ruby脚本。试了两种方法都卡壳:
- 想用
org.jruby在Scala里直接跑Ruby脚本,但build.sbt里的依赖配置一直出问题; - 尝试把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调用脚本:
build.sbt添加正确依赖:
libraryDependencies ++= Seq( "org.jruby" % "jruby-core" % "9.4.5.0", "org.jruby" % "jruby-stdlib" % "9.4.5.0" )
- 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
相关产品推荐
相关产品推荐

