我正在为Shopify webhook实现一个简单的Web服务,以便使用Play2进行调用。我想使用包含的'X-Shopify-Hmac-Sha256'标题参数验证来自Shopify的电话。
Shopify文档只包含Ruby和Php示例,我想不太难翻译。好吧,我似乎在挣扎。
这是我的简单Scala shopify util对象:
import play.api.mvc.Request
import play.api.mvc.AnyContent
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import play.api.Logger
import javax.crypto.SecretKey
import org.apache.commons.codec.binary.Base64
object ShopifyUtils {
def verifyWebhookCall(request : Request[AnyContent], secretKey: String) : Boolean = {
if (!request.headers.get("X-Shopify-Hmac-Sha256").isDefined)
false
else
{
val headerHash = request.headers.get("X-Shopify-Hmac-Sha256").getOrElse("")
val body = request.body.asJson.get.toString
Logger.info("json '" + request.body.asJson.get.toString + "' = " + encode(secretKey, request.body.asJson.get.toString) );
Logger.info("body '" + request.body.toString() + "' = " + encode(secretKey, request.body.toString) )
Logger.info("headerHash " + headerHash);
val calcHash = encode(secretKey, body)
headerHash.equals(calcHash)
}
}
def encode(key: String , data: String): String = {
val sha256_HMAC = Mac.getInstance("HmacSHA256");
val secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
return new String( Base64.encodeBase64( sha256_HMAC.doFinal( data.getBytes ) ) ).trim
}
}
我生成的哈希与Shopify发送的哈希不一样。
我的共享密钥是错误的(我不知道它是怎么回事)或者我没有像Shopify那样散列相同的内容(我尝试了各种request.body
输出格式)。 / p>
感激不尽的任何提示/指南/建议。
添
答案 0 :(得分:2)
只需阅读原始POST正文并运行验证您的签名。通过将主体抓取为JSON并将其转换为字符串,您可能会巧妙地操纵我们发送给您的响应。
以下是我为一些我使用过webhooks(在ruby中)的项目所做的工作:
class WebhookVerifier
attr_accessor :expected_hmac, :data
def initialize(options = {})
@expected_hmac = options.fetch(:expected_hmac, '')
content = options.fetch(:content, StringIO.new)
content.rewind
@data = content.read
end
def valid?
digest = OpenSSL::Digest::Digest.new('sha256')
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, ShopifyApp.configuration.secret, data)).strip
calculated_hmac == expected_hmac
end
end
答案 1 :(得分:1)
感谢csaunders指出我正确的方向。
我使用默认的BodyParser AnyContent
,当请求的Content-type指定'application / json'时,隐式地将响应主体转换为json。
我必须修改我的控制器对象以指定'raw'BodyParser:
import play.api._
import play.api.libs.iteratee.Enumerator
import play.api.mvc.SimpleResult
import play.api.mvc.ResponseHeader
import play.api.libs.json._
import play.Application
import play.api.mvc._
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import play.api.Logger
import javax.crypto.SecretKey
import org.apache.commons.codec.binary.Base64
object Purchase extends Controller {
val shopifyAppSecretKey = "11111111111111111111111111111111"
def processPurchase() = Action( parse.raw ) {request =>
val bodyRaw = request.body.asBytes(3000).getOrElse(Array[Byte]())
val calculatedHash = encodeByteArray(shopifyAppSecretKey, bodyRaw)
val shopifyHash = request.headers.get("X-Shopify-Hmac-Sha256").getOrElse("")
Logger.info("keys '" + shopifyHash + "' || '" + calculatedHash + "' " + calculatedHash.equals(shopifyHash))
val json: JsValue = Json.parse( new String(bodyRaw) )
Ok( "Ok" ).as(HTML)
}
def encodeByteArray(key: String , data: Array[Byte]): String = {
val sha256_HMAC = Mac.getInstance("HmacSHA256");
val secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
return new String( Base64.encodeBase64( sha256_HMAC.doFinal( data ) ) ).trim
}
}
使用'raw'BodyParser意味着您必须自己将字节数组转换为字符串,然后手动解析该字符串以获取您的json,但这不是真正的问题。
现在一切正常。
谢谢,
添