使用Python针对GCS PUT请求在GAE上签名的URL

ham*_*x0r 5 python google-app-engine python-2.7 google-cloud-storage

我正在尝试创建一个签名的URL,用于将文件直接上传到Google Cloud Storage(GCS)。我使用这个Github示例使用POST来工作,该示例利用了策略。按照最佳实践,我正在重构以使用PUT并出现SignatureDoesNotMatch错误:

<?xml version='1.0' encoding='UTF-8'?><Error><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><StringToSign>PUT


123456789
/mybucket/mycat.jpg</StringToSign></Error>
Run Code Online (Sandbox Code Playgroud)

根据有关使用程序GCP示例Python代码创建签名URL的文档,我正在执行以下过程:

  1. 建立我的签名串
  2. 签名
  3. base64编码
  4. url对结果进行编码(尽管python示例未执行此操作...

由于此程序在Google App Engine(GAE)应用程序上运行,因此我不需要为服务帐户用户获取JSON密钥文件,而应使用App Identity Services对其进行签名。这是Flask项目中的代码:

google_access_id = app_identity.get_service_account_name()
expires = arrow.utcnow().replace(minutes=+10).replace(microseconds=0).timestamp
resource = '/mybucket/mycat.jpg'

args = self.get_parser.parse_args()
signature_string = 'PUT\n'
# take MD5 of file being uploaded and its content type, if provided
content_md5 = args.get('md5') or ''
content_type = args.get('contenttype') or ''
signature_string = ('PUT\n'
                    '{md5}\n'
                    '{content_type}\n'
                    '{expires}\n'
                    '{resource}\n').format(
                    md5=content_md5,
                    content_type=content_type,
                    expires=expires,
                    resource=resource)
log.debug('signature string:\n{}'.format(signature_string))
_, signature_bytes = app_identity.sign_blob(signature_string)
signature = base64.b64encode(signature_bytes)
# URL encode signature
signature = urllib.quote(signature)
media_url = 'https://storage.googleapis.com{}'.format(resource)
return dict(GoogleAccessId=google_access_id,
            Expires=expires,
            Signature=signature,
            bucket='mybucket',
            media_url='{}?GoogleAccessId={}&Expires={}&Signature={}'.format(media_url, google_access_id, expires, signature))
Run Code Online (Sandbox Code Playgroud)

log.debug语句将打印一个签名文件,该文件与上面的GCS XML错误响应中的签名完全匹配。如果匹配,为什么我不能上传?

使用gsutil,我可以使用相同的GAE服务帐户创建一个签名的URL,并且在Postman中可以正常工作。我看到gsutilURL对签名进行了编码,但是在创建自己的签名URL时,这似乎都不重要:GCS收到我的PUT请求,并抱怨签名不匹配,即使它显示的签名与我的签名匹配也是如此。记录的调试消息。我还尝试过在原始签名字符串中使用尾随\ n的情况,也可以不使用尾随的\ n。

编辑:POST我跟随Base64 的示例在策略唱之前对策略进行编码然后在对它进行签名之后对策略进行编码。我在创建PUT签名时尝试了这种方法,但没有区别

ham*_*x0r 1

答案与 SO 和其他地方找到的其他答案非常接近,指出Content-Type需要使用标头。这在一定程度上是正确的,但被我的主要问题所掩盖:我依赖于默认 GAE 服务帐户,该帐户具有“编辑器”权限,我认为该帐户可以读取和写入 GCS。我从该帐户创建了一个密钥文件并使用它,gsutil然后给了我这个线索:

myDefaultGAEserviceaccount@appspot.gserviceaccount.com does not have permissions on gs://mybucket/cat.jpg, using this link will likely result in a 403 error until at least READ permissions are granted
Run Code Online (Sandbox Code Playgroud)

尝试将文件放在那里时,我确实收到了 GCS 的错误,但这不是 404 错误,而是SignatureDoesNotMatch问题中显示的错误。

解决方案分为两部分:

  1. 为默认 GAE 服务帐户授予“存储”权限集中的更多权限:
    1. 存储对象创建者
    2. 存储对象查看器
  2. 将文件放入 GCS 时请务必使用Content-Type标头,因为即使我没有在要签名的签名字符串中指定它,它也会默认text/plain并要求在标头中进行指定。

最后一个观察:即使在我向帐户添加了正确的权限后,我仍然收到 gsutil 警告,直到我从 IAM 控制台创建新的 JSON 密钥文件。SignatureDoesNotMatch此外,即使在 IAM 控制台上设置了权限,使用 gsutil 和旧密钥文件创建的签名 URL 也无法工作(错误)。在 GAE 上运行的 Python 代码运行良好,无需任何更新 - 它只需要 IAM 中设置的权限,然后匹配内容标头。