如何使用 Elixir 或 Erlang 创建一个 Google 签名的 URL 云存储?

How to create a Google Cloud Storage Signed URL using Elixir or Erlang?

我目前正在尝试让我的 elixir 网络服务器为 Google 云存储生成签名 url,以便我可以生成过期的文件 url。 不幸的是,当我尝试使用生成的网址时,出现以下错误:

<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.
</Message>

我可以通过 gsutil 工具生成签名的 url,尽管它很慢,也可以通过这里给出的 python 示例:

Google Cloud Storage Signed URLs Example

我目前在 Elixir 中的实现基于上面的 Python 示例,看起来像这样:

@default_expiration 1000
  def construct_string(http_verb, content_md5, content_type, expiration_timestamp, canonicalized_extension_headers, canonicalized_resource) do
    "#{http_verb}\n
    #{content_md5}\n
    #{content_type}\n
    #{expiration_timestamp}\n
    #{canonicalized_extension_headers}
    #{canonicalized_resource}"
  end

  def load_secret_pem do
    load_local_key_file("/path/to/key")
  end

  def load_local_key_file(path) do
    {ok, pem_bin} = File.read(path)
    [rsa_entry] = :public_key.pem_decode(pem_bin)
    key = :public_key.pem_entry_decode(rsa_entry)
  end

  def base64Sign(plaintext) do
    key = load_secret_pem()
    signature_bytes = :public_key.sign(plaintext, :sha256, key )
    Base.url_encode64(signature_bytes)
    |> String.replace("-", "%2B")
    |> String.replace("_", "%2F")
    |> URI.encode_www_form
  end

  def make_url(verb, path, content_md5 \ "", content_type \ "") do
    client_id = GCloud.Credentials.client_email() |> URI.encode_www_form
    expiration =  :os.system_time(:seconds) + @default_expiration
    base_url = GCloud.Storage.base_uri() <> path
    signature_string = construct_string(verb, content_md5, content_type, expiration, "", path )
    url_encoded_signature = base64Sign(signature_string)
    IO.puts "#{base_url}?GoogleAccessId=#{client_id}&Expires=#{expiration}&Signature=#{url_encoded_signature}"
  end

如何使用 Elixir 或 Erlang 对已签名的 url 进行正确签名?

您在 construct_string 中的字符串构造可能在做您没有意识到的事情。记得Python语法不一样,对空格有其他看法。

defmodule Test do
  def foo(a,b) do
    "#{a}\n
    #{b}"
  end
end
IO.inspect Test.foo(1,2)
# output:
"1\n\n    2"

如果您改用带有 """ 的 heredoc,前导空格会消失,但您的换行符仍然会重复。但是,这种方法可能不是一个好主意,因为如果您从 windows 机器上保存文件,您可能会以 \r\n 作为编辑器的结尾行,而删除这些是不必要的麻烦无论如何。

相反,我认为您应该将此处的方法更改为如下所示:

def construct_string(http_verb, content_md5, content_type, expiration_timestamp, canonicalized_extension_headers, canonicalized_resource) do
  headers = Enum.join([http_verb, content_md5, content_type, expiration_timestamp], "\n")
  "#{headers}\n#{canonicalized_extension_headers}#{canonicalized_resource}"
end

我不确定是否还有其他错误,但我立即注意到了这一点。

我设法让它工作,我通过并排打开 python 和 elixir REPL 来做到这一点,用测试字符串执行两个步骤中的每个步骤并比较输出的差异,那里没有差异在对测试字符串进行哈希或签名之后,但是有base64编码之后,所以我改变了:

def base64Sign(plaintext) do
    key = load_secret_pem()
    signature_bytes = :public_key.sign(plaintext, :sha256, key )
    Base.url_encode64(signature_bytes)
    |> String.replace("-", "%2B")
    |> String.replace("_", "%2F")
    |> URI.encode_www_form
end

def base64Sign(plaintext) do
    key = GCloud.Credentials.load_secret_pem()
    signature_bytes = :public_key.sign(plaintext, :sha256, key )
    Base.encode64(signature_bytes)
    |> URI.encode_www_form
end

结合 解决了这个问题。

使用 gcs_signer 来自 hex.pm:

Application.get_env(:goth, :json)
|> Poison.decode!
|> GcsSigner.Client.from_keyfile()
|> GcsSigner.sign_url(bucket, object)

在Google Cloud Platform 环境中,例如 Cloud Functions 和 App 引擎,您通常不提供 keyFilenamecredentials 实例化,
那么你可以使用 signBlob api see this