连接Azure Blob存储API出错:R语言环境下的解决方案

6

我正尝试通过REST API在R中使用Azure存储。我正在使用httr包,它覆盖了Curl。

设置

您可以使用R-fiddle:http://www.r-fiddle.org/#/fiddle?id=vh8uqGmM

library(httr)
requestdate<-format(Sys.time(),"%a, %d %b %Y %H:%M:%S GMT")
url<-"https://preconstuff.blob.core.windows.net/pings?restype=container&comp=list"
sak<-"Q8HvUVJLBJK+wkrIEG6LlsfFo19iDjneTwJxX/KXSnUCtTjgyyhYnH/5azeqa1bluGD94EcPcSRyBy2W2A/fHQ=="
signaturestring<-paste0("GET",paste(rep("\n",12),collapse=""),
"x-ms-date:",requestdate,"
x-ms-version:2009-09-19
/preconstuff/pings
comp:list
restype:container")

headerstuff<-add_headers(Authorization=paste0("SharedKey preconstuff:",
                         RCurl::base64(digest::hmac(key=sak,
                         object=enc2utf8(signaturestring),
                         algo= "sha256"))),
                    `x-ms-date`=requestdate,
                    `x-ms-version`= "2009-09-19")

尝试列出 blobs:

content(GET(url,config = headerstuff, verbose() ))

错误

最高层消息

在HTTP请求中找到的MAC签名“Q8HvUVJLBJK+wkrIEG6LlsfFo19iDjneTwJxX/KXSnUCtTjgyyhYnH/5azeqa1bluGD94EcPcSRyBy2W2A/fHQ==”与任何计算出的签名都不相同。

响应内容

[1] "<?xml version=\"1.0\" encoding=\"utf-8\"?><Error>
<Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\nRequestId:1ab26da5-0001-00dc-6ddb-15e35c000000\nTime:2015-03-26T17:51:42.7190620Z</Message>
<AuthenticationErrorDetail>The MAC signature found in the HTTP request 'NTM1ODZjMjhhZmMyZGM3NDM0YTFjZDgwNGE0ODVmMzVjNDhkNjBkNzk1ZjNkZjJjOTNlNjUxYTMwMjRhNzNlYw==' is not the same as any computed signature. Server used following string to sign: 
'GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:Thu, 26 Mar 2015 17:52:37 GMT\nx-ms-version:2009-09-19\n/preconstuff/pings\ncomp:list\nrestype:container'.
</AuthenticationErrorDetail></Error>"

详细输出

-> GET /pings?restype=container&comp=list HTTP/1.1
-> User-Agent: curl/7.39.0 Rcurl/1.95.4.5 httr/0.6.1
-> Host: preconstuff.blob.core.windows.net
-> Accept-Encoding: gzip
-> Accept: application/json, text/xml, application/xml, */*
-> Authorization: SharedKey preconstuff:OTRhNTgzYmY3OTY3M2UzNjk3ODdjMzk3OWM3ZmU0OTA4MWU5NTE2OGYyZGU3YzRjNjQ1M2NkNzY0ZTcyZDRhYQ==
-> x-ms-date: Thu, 26 Mar 2015 17:56:27 GMT
-> x-ms-version: 2009-09-19
-> 
<- HTTP/1.1 403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
<- Content-Length: 719
<- Content-Type: application/xml
<- Server: Microsoft-HTTPAPI/2.0
<- x-ms-request-id: 3d47770c-0001-0085-2313-6d466f000000
<- Date: Thu, 26 Mar 2015 17:56:27 GMT
<- 

解决错误

通过谷歌搜索,似乎找不到一个一致的原因,但很可能是由于我方格式或请求结构不当所致。为此,我进行了以下检查:

  1. 我已验证我的密钥是正确的(它只是从门户中复制并粘贴)
  2. 我已确保日期格式正确
  3. 有一个最近的documentDB SO建议这可能是时钟偏差问题,我注意到我的x-ms-date比响应中的日期提前了一秒。我尝试发送一个明确在过去但在15分钟容差范围内的固定值。没有改变信息。
  4. 根据MSDN论坛问题,在headerstuff中添加了encoding="Base64",但返回了相同的错误消息
  5. 根据@Serdar的回答,我加入了构建签名字符串的过程(我已经验证这与错误消息中提供的签名字符串匹配),然后对UTF8转换的signaturestring进行hmac-sha256的base64编码(使用辅助访问密钥(sak)作为加密密钥)作为SharedKey授权中要使用的值。
  6. 根据@Serdar的评论,签名字符串和主请求中使用的日期必须相同,因此定义一次并重复使用

有什么明显的问题吗?还有其他需要检查的事项吗?代码对其他人是否有效?

3个回答

4

看起来你的问题在于密钥。你提供的密钥字符串实际上是base64编码的。在使用它签名请求之前,你需要将其解码为原始向量。例如:

url<-"https://preconstuff.blob.core.windows.net/pings?restype=container&comp=list"
sak<-"Q8HvUVJLBJK+wkrIEG6LlsfFo19iDjneTwJxX/KXSnUCtTjgyyhYnH/5azeqa1bluGD94EcPcSRyBy2W2A/fHQ=="

requestdate<-format(Sys.time(),"%a, %d %b %Y %H:%M:%S %Z", tz="GMT")
signaturestring<-paste0("GET",paste(rep("\n",12),collapse=""),
"x-ms-date:",requestdate,"
x-ms-version:2009-09-19
/preconstuff/pings
comp:list
restype:container")

headerstuff<-add_headers(Authorization=paste0("SharedKey preconstuff:",
                         RCurl::base64(digest::hmac(key=RCurl::base64Decode(sak, mode="raw"),
                         object=enc2utf8(signaturestring),
                         algo= "sha256", raw=TRUE))),
                    `x-ms-date`=requestdate,
                    `x-ms-version`= "2009-09-19")

content(GET(url,config = headerstuff, verbose() ))

这种方式没有更多的身份验证错误,但是没有列出任何blob。也许这是一个不同的问题。
此外,我改变了日期/时间创建的方式,以更安全地将本地时间更改为GMT。

你,@MrFlick,是个天才!干杯。没有 blob,因为我首先尝试 GET 而不是 PUT。 - Steph Locke
这也是连接到DocumentDB集合的工作解决方案(使用一些不同的标头)。感谢@MrFlick!! - bAN

2
看起来您正在直接在授权头中使用帐户的密钥。要验证请求,您必须使用发出请求的帐户的密钥对请求进行签名,并将该签名作为请求的一部分传递。有关如何构造授权标头的详细信息,请参见Azure Storage Services的身份验证
请注意,服务在错误响应中返回StringToSign。因此,您的代码应该对StringToSign="GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:Wed, 25 Mar 2015 22:24:12 GMT\nx-ms-version:2014-02-14\n/preconstuff/pings\ncomp:list\nrestype:container"(不带引号)应用以下公式:
Signature=Base64(HMAC-SHA256(AccountKey, UTF8(StringToSign)))

服务如何计算StringToSign,在上面分享的链接中有详细解释。

感谢您的快速回复,我仍在阅读BOL的那一部分,但还是不理解。看到构建签名字符串的部分,我非常困惑(我对API相对陌生),而且没有URL与VERB相对应。您能否大致描述一下您如何看待调用过程?我可以从那里开始并为您完善答案。 - Steph Locke
我刚刚在答案中添加了更多信息。我不懂R,所以没有示例代码,但是你的动词基本上是GET。请尝试将文档与服务返回的StringToSign进行比较。然后,您应该使用您的帐户密钥对该字符串进行签名。 - Serdar Ozler
认为我按照指示在做,但还是不起作用 :-/ 问题更新了最新的代码,并加入了StringToSign。 - Steph Locke
  1. 你正在两次调用 Sys.time()。你添加到 StringToSign 和头部的日期必须完全匹配,因为 StringToSign 应该包含这些头部的确切值。
  2. 即使在 URL 中没有超时,为什么你还要将超时添加到 StringToSign 中?
- Serdar Ozler

1

我通过上面的MrFlick的示例代码进行了工作,并为了使其工作,我不得不做出一些更改。

日期字符串必须设置为英语区域设置,例如:

lct <- Sys.getlocale("LC_TIME") 
Sys.setlocale("LC_TIME", "us")
requestdate <- format(Sys.time(),"%a, %d %b %Y %H:%M:%S %Z", tz="GMT")
Sys.setlocale("LC_TIME", lct)

“signaturestring” 应使用 \n 在参数之间进行格式化:
 signaturestring <- paste0("GET", paste(rep("\n", 12), collapse=""),
"x-ms-date:", requestdate, 
"\nx-ms-version:2009-09-19\n/preconstuff/pings\ncomp:list\nrestype:container")

编辑:以下步骤适用于我。基于Steph Locke的示例。

library(httr)
library(RCurl)

azureBlobCall <- function(url, verb, key, requestBody=NULL, headers=NULL, ifMatch="", md5="") { 
  urlcomponents <- httr::parse_url(url)
  account <- gsub(".blob.core.windows.net", "", urlcomponents$hostname, fixed = TRUE)
  container <- urlcomponents$path

  # get timestamp in us locale
  lct <- Sys.getlocale("LC_TIME"); Sys.setlocale("LC_TIME", "us")
  `x-ms-date` <- format(Sys.time(),"%a, %d %b %Y %H:%M:%S %Z", tz="GMT")
  Sys.setlocale("LC_TIME", lct)

  # if requestBody exist get content length in bytes and content type
  `Content-Length` <- ""; `Content-Type` <- ""
  if(!is.null(requestBody)) {
    if(class(requestBody) == "form_file") {
      `Content-Length` <- (file.info(requestBody$path))$size
      `Content-Type` <- requestBody$type 
    } else {
      requestBody <- enc2utf8(as.character(requestBody))
      `Content-Length` <- nchar(requestBody, "bytes")
      `Content-Type` <- "text/plain; charset=UTF-8" 
    }
  } 

  # combine timestamp and version headers with any input headers, order and create the CanonicalizedHeaders
  headers <- setNames(c(`x-ms-date`, "2015-04-05",  unlist(headers)), 
                      c("x-ms-date", "x-ms-version", unclass(names(unlist(headers)))))
  headers <- headers[order(names(headers))]
  CanonicalizedHeaders <- paste(names(headers), headers, sep=":", collapse = "\n")

  # create CanonicalizedResource headers and add any queries to it
  if(!is.null(urlcomponents$query)) {
    components <- setNames(unlist(urlcomponents$query), unclass(names(unlist(urlcomponents$query))))
    componentstring <- paste0("\n", paste(names(components[order(names(components))]),
                                          components[order(names(components))], sep=":", collapse = "\n"))
  } else componentstring <- ""
  CanonicalizedResource <- paste0("/",account,"/",container, componentstring)

  # create the authorizationtoken
  signaturestring <- paste0(verb, "\n\n\n", `Content-Length`, "\n", md5, "\n", `Content-Type`, "\n\n\n", 
                            ifMatch, "\n\n\n\n", CanonicalizedHeaders, "\n", CanonicalizedResource)

  requestspecificencodedkey <- RCurl::base64(
    digest::hmac(key=RCurl::base64Decode(key, mode="raw"),
                 object=enc2utf8(signaturestring),
                 algo= "sha256", raw=TRUE)
  )

  authorizationtoken <- paste0("SharedKey ", account, ":", requestspecificencodedkey)

  # make the call
  headers_final <- add_headers(Authorization=authorizationtoken, headers, `Content-Type` = `Content-Type`)
  call <- httr::VERB(verb=verb, url=url, config=headers_final, body=requestBody, verbose())

  print("signaturestring");print(signaturestring); 
  print(headers_final); print(call)
  return(content(call))
} 

## Tests. Replace 'key' and 'accountName' with yours
key <- "YowThr***********RDw=="

# Creates a container named 'test'
azureBlobCall("https://accountName.blob.core.windows.net/test?restype=container", "PUT", key)
# Creates a blob named 'blob' under container 'test' with the content of "Hej världen!"
azureBlobCall("https://accountName.blob.core.windows.net/test/blob", "PUT", key, 
              headers = c("x-ms-blob-type"="BlockBlob"), requestBody = "Hej världen!") #upload_file("blob.txt"))
# List all blob in the container 'test'
azureBlobCall("https://accountName.blob.core.windows.net/test?comp=list&restype=container", "GET", key)
# deletes the blobl named 'blob' 
azureBlobCall("https://accountName.blob.core.windows.net/test/blob", "DELETE", key)
# Creates a blob named 'blob' under container 'test' with and upload the file 'blob.txt'
azureBlobCall("https://accountName.blob.core.windows.net/test/blob", "PUT", key, 
              headers = c("x-ms-blob-type"="BlockBlob"), requestBody = upload_file("blob.txt"))
# deletes the container named 'test' 
azureBlobCall("https://accountName.blob.core.windows.net/test?restype=container", "DELETE", key)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接