javascript - 如何在 NodeJS 中对 Azure 表存储执行 PUT 或 DELETE?
问题描述
所以我一直在做一个项目,我想为 Azure 表存储构建基本的 CRUD 类功能,但是我一直卡在生成有效的 SharedKeyLite 签名上。
我能够为 GET 和 POST 生成有效的签名,但是当我出于某种奇怪的原因想要 PUT 或 DELETE 和实体时,签名现在无效?
const axios = require('axios');
const client = axios.create({});
const SharedKeyGenerate = require('./SharedKeyGenerator');
const AZURE_STORAGE_KEY = 'STORAGE_KEY';
const AZURE_STORAGE_ACCOUNT = 'igdevharuntest';
const sharedKey = new SharedKeyGenerate(AZURE_STORAGE_ACCOUNT, 'powerbiTableStorage', AZURE_STORAGE_KEY);
client.defaults.baseURL = `https://${AZURE_STORAGE_ACCOUNT}.table.core.windows.net`;
client.defaults.timeout = 2000;
/**
* get tables from azure table storage
* with optional filter
* @param {string} path uri path
* @param {string} filter filter for the web request
*/
async function getTables(path, filter) {
try {
const response = await client.get(`/${path}${filter}`, {
headers: {
'x-ms-date': new Date().toUTCString(),
'Authorization': sharedKey.GenerateSharedKeyLite(path, 'GET', client),
Accept: 'application/json;odata=nometadata',
'x-ms-version': '2015-12-11',
}
});
console.log(response.data);
} catch (err) {
console.log(err.response.data);
}
}
async function addEntityToTable(tableName) {
const payload = {
'expiresOn': '2019-10-16T17:27:36.046Z',
'accessToken': 'abcsd123456',
'PartitionKey': 'NewPartitionKey12',
'RowKey': 'CompletelyNewKey12'
};
try {
const response = await client.post(`/${tableName}`, payload,{
headers: {
'x-ms-date': new Date().toUTCString(),
'Authorization': sharedKey.GenerateSharedKeyLite(tableName, "POST"),
Accept: 'application/json;odata=nometadata',
'x-ms-version': '2015-12-11',
}
});
console.log(response.data);
} catch (err) {
console.log(err.response);
}
}
async function updateEntity(path, filter) {
const payload = {
"PartitionKey": "ABCDEFG1234567RANDOMPARTITIONKEY1111",
"RowKey": "ABCDEFG1234567",
"expiresOn": "2019-10-16T17:37:07.099Z",
"accessToken": "eyJ0eXAiOiJKV1QiLCjjJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL2FuYWx5c2lzLndpbmRvd3MubmV0L3Bvd2VyYmkvYXBpIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvMWMxNzY3MTItMTVmNy00MThjLTk1YWMtNDZmNmE2YTU5NWM1LyIsImlhdCI6MTU3MTI0MzUyNywibmJmIjoxNTcxMjQzNTI3LCJleHAiOjE1NzEyNDc0MjcsImFjY3QiOjAsImFjciI6IjEiLCJhaW8iOiJBU1FBMi84TkFBQUFCQUR6RVIrbHdDT3hPNFAzNzlMQ1dGSjNUUHVVN0Zud1FGaWM2aVpyU05VPSIsImFtciI6WyJwd2QiXSwiYXBwaWQiOiI1NzEzNmRkYy00ZjI3LTQyZTAtYWZmMS1iOGFkOTVlODc0MmEiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlNoZWlraGFsaSIsImdpdmVuX25hbWUiOiJIYXJ1biIsImlwYWRkciI6IjcyLjE0Mi4xOC4zOCIsIm5hbWUiOiJIYXJ1biBTaGVpa2hhbGkiLCJvaWQiOiJjOGZjZDhmNS1jMThmLTQwMGQtODk1OS1jNjFhMjA1NDE1OTYiLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjcyMzg1MTQwOC0zODY3MzY4NjQwLTEyMjI5NzYyMDMtMzg0NyIsInB1aWQiOiIxMDAzMDAwMDlBQzQ2QUY1Iiwic2NwIjoiQ2FwYWNpdHkuUmVhZC5BbGwgQ2FwYWNpdHkuUmVhZFdyaXRlLkFsbCBDb250ZW50LkNyZWF0ZSBEYXNoYm9hcmQuUmVhZC5BbGwgRGFzaGJvYXJkLlJlYWRXcml0ZS5BbGwgRGF0YS5BbHRlcl9BbnkgRGF0YXBvb2wuUmVhZC5BbGwgRGF0YXBvb2wuUmVhZFdyaXRlLkFsbCBEYXRhc2V0LlJlYWQuQWxsIERhdGFzZXQuUmVhZFdyaXRlLkFsbCBHcm91cC5SZWFkIEdyb3VwLlJlYWQuQWxsIE1ldGFkYXRhLlZpZXdfQW55IFJlcG9ydC5SZWFkLkFsbCBSZXBvcnQuUmVhZFdyaXRlLkFsbCBXb3Jrc3BhY2UuUmVhZC5BbGwgV29ya3NwYWNlLlJlYWRXcml0ZS5BbGwiLCJzdWIiOiJkQzlDN1hSbnktdHcyWGc2QkRqYUZULWg3cmo1T05lN2VkOWV6dTZqWmQ4IiwidGlkIjoiMWMxNzY3MTItMTVmNy00MThjLTk1YWMtNDZmNmE2YTU5NWM1IiwidW5pcXVlX25hbWUiOiJoc2hlaWtoYWxpQGlnbG9vc29mdHdhcmUuY29tIiwidXBuIjoiaHNoZWlraGFsaUBpZ2xvb3NvZnR3YXJlLmNvbSIsInV0aSI6ImxhYUd6UWVKRDBxZFVTbDZtRjRmQUEiLCJ2ZXIiOiIxLjAifQ.prKqNPn75_CWdbLvsl5VVvuZAK-PEI2n1DlU4gFayt_eLPzllZUlEpVqIgVgTAzeccYEj5Z6vBKpMjXT7ftwCVjnKQitidGILehaEfrWiXX3xU4ZatPQ_TNc6Y6NzMyIQTWAbPkCHfpFnBlbAD0xp9Kl-bpAq_QXbl4yIa6_IQMRMwi5WdWd8WJLLdxKQTkWiKkGBBl-La3wgYrWfzBXMzBLhlfMk_vqsOyJdg1jOUEnUmScqxKh5DUR5DvoRtdeVxc2rDz1GWM8MTdhviB0CRub7bKeMA35rLzEui69L4o8gT_FuXLXqvVLDL9sq7OZNX8q3BL-VxLkq6GSOpnkcg"
};
try {
const response = await client.put(`/${path}${filter}`, payload,{
headers: {
'x-ms-date': new Date().toUTCString(),
'Accept-Charset': 'UTF-8',
'Authorization': sharedKey.GenerateSharedKeyLite(path, "PUT"),
Accept: 'application/json;odata=nometadata',
'x-ms-version': '2015-12-11',
'Content-Type': 'application/json',
'If-Match': '*'
}
});
console.log(response);
} catch (err) {
console.log(err.response.data);
}
}
async function deleteEntity(path, filter, partitionKey, rowKey) {
try {
const response = await client.delete(`/${path}${filter}`, {
headers: {
'x-ms-date': new Date().toUTCString(),
'Accept-Charset': 'UTF-8',
'Authorization': sharedKey.GenerateSharedKeyLite(path),
Accept: 'application/json;odata=nometadata',
'x-ms-version': '2015-12-11',
'Content-Type': 'application/json',
'If-Match': '*'
}
});
console.log(response);
} catch (err) {
console.log(err.response.data);
}
}
//Works
getTables("powerbiTableStorage()", "?$filter=PartitionKey%20eq%20'ABCDEFG1234567RANDOMPARTITIONKEY1111'%20and%20RowKey%20eq%20'ABCDEFG1234567'");
//Works
addEntityToTable('powerbiTableStorage()');
//does not work
updateEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111',RowKey='ABCDEFG1234567')", "?$filter=PartitionKey%20eq%20'ABCDEFG1234567RANDOMPARTITIONKEY1111'%20and%20RowKey%20eq%20'ABCDEFG1234567'");
//does not work
deleteEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111',RowKey='ABCDEFG1234567')", "");
这是错误消息:{ 'odata.error': { code: 'AuthenticationFailed', message: { lang: 'en-US', value: '服务器未能验证请求。确保 Authorization 标头的值格式正确,包括签名。\n' + 'RequestId:d5684fff-8002-001c-0dd9-9fe2c5000000\n' + 'Time:2019-11-20T19:31:28.7740552Z' } } }
有人可以帮我吗?
快速更新...
这是我运行 GET 请求时得到的
stringToSign:2019 年 11 月 21 日星期四 15:48:07 GMT /igdevharuntest/Tables
签名:SharedKeyLite igdevharuntest:PxpjkL+WwN7ZtHD1NXctjtMKSdWuAjNY3xS3jo7/n/Q=
{
value: [
{ TableName: 'photowallCache' },
{ TableName: 'powerbiTableStorage' },
{ TableName: 'tblProjectStatus' }
]
}
共享密钥生成:
const Utf8 = require('crypto-js/enc-utf8');
const Base64 = require('crypto-js/enc-base64');
const hmacSHA256 = require('crypto-js/hmac-sha256');
class SharedKeyGenerator {
constructor(storageAccount, tableName, storageKey) {
this.storageAccountName = storageAccount;
this.storageAccountKey = storageKey;
this.tableName = tableName;
}
/**
* generates a shared key lite for authorization
*
* @param path path of the resource
* @returns {string} signed signature
*/
GenerateSharedKeyLite(path) {
const date = new Date().toUTCString();
let stringToSign = date + '\n' + this._getCanonicalizedResource(path);
const hash = hmacSHA256(Utf8.parse(stringToSign), Base64.parse(this.storageAccountKey));
const signature = Base64.stringify(hash);
console.log(stringToSign);
console.log('SharedKeyLite ' + this.storageAccountName + ':' + signature);
return 'SharedKeyLite ' + this.storageAccountName + ':' + signature;
}
_getCanonicalizedResource(path) {
return `/${this.storageAccountName}/${path}`;
}
}
module.exports = SharedKeyGenerator;
这是我执行 PUT 请求时得到的结果
stringToSign:格林威治标准时间 2019 年 11 月 21 日星期四 15:48:07 /igdevharuntest/powerbiTableStorage(PartitionKey=NewPartitionKey12,RowKey=CompletelyNewKey12)
签名:SharedKeyLite igdevharuntest:dt14aw/McZet9JDiZDxnrZnwfMm8AfZZ7jNnTjVJ71A=
{
'odata.error': {
code: 'AuthenticationFailed',
message: {
lang: 'en-US',
value: 'Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\n' +
'RequestId:a9f6868a-c002-001b-6c83-a01440000000\n' +
'Time:2019-11-21T15:48:07.3247287Z'
}
}
}
- - 更新 - - -
所以我注意到,当我使用括号中的 PartitionKey 和 RowKey 值执行 GET 时,我得到一个 AuthenticationFailed 错误。
例如:uri = GET:https://{azure_storage_account}.table.core.windows.net/TableStorageName(PartitionKey='PartitionKey',RowKey='RowKey')
回应是
{
'odata.error': {
code: 'AuthenticationFailed',
message: {
lang: 'en-US',
value: 'Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\n' +
'RequestId:a9f6868a-c002-001b-6c83-a01440000000\n' +
'Time:2019-11-21T15:48:07.3247287Z'
}
}
}
如果我保持括号为空,但添加一个过滤器作为查询参数,这工作得很好,我得到了我的表的特定实体。
例如:GET:https://{azure_storage_account}.table.core.windows.net/TableStorageNam()?$filter=PartitionKey eq 'PartitionKey' 和 RowKey eq 'RowKey'
回应是:
{
value: [
{
PartitionKey: 'PartitionKey',
RowKey: 'RowKey',
Timestamp: '2019-11-20T18:16:24.5881171Z',
expiresOn: '2019-10-16T17:27:36.046Z',
accessToken: 'abcsd123456'
}
]
}
我在想我构建规范化资源的方式对于括号内的值可能不正确?我不知道..我现在被困住了。我所有的标题值都是正确的,所以我认为这不是问题。
解决方案
更新:
这是由于在调用更新/删除函数时缺少 partitionKey 和 RowKey 之间的空格。
当调用删除函数时,应该改变
`deleteEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111',RowKey='ABCDEFG1234567')", "");`
到
`deleteEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111', RowKey='ABCDEFG1234567')", "");`
当调用更新函数时,应该改变
updateEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111',RowKey='ABCDEFG1234567')", "?$filter=PartitionKey%20eq%20'ABCDEFG1234567RANDOMPARTITIONKEY1111'%20and%20RowKey%20eq%20'ABCDEFG1234567'");
到
updateEntity("powerbiTableStorage(PartitionKey='ABCDEFG1234567RANDOMPARTITIONKEY1111', RowKey='ABCDEFG1234567')", "");
对于删除:
const request = require("request");
const crypto = require("crypto");
const url = require('url')
var accountName = "xxx";
var accountKey = "xxx";
var tableName = "test22";
var pk = "r1";
var rk = "s5";
const encodedUriPath = tableName + '(PartitionKey=' + '\'' + pk + '\'' + ', ' + 'RowKey=' + '\'' + rk + '\'' + ')';
console.log(encodedUriPath)
const endpoint = "https://" + accountName + ".table.core.windows.net/" + encodedUriPath;
const parsedUrl = url.parse(endpoint);
const timestamp = (new Date()).toUTCString();
console.log(url);
console.log(timestamp);
const stringToSign = timestamp + '\n/' + accountName + parsedUrl.path;
console.log('--------------------------------------');
console.log(stringToSign);
const hmac = crypto.createHmac('sha256', new Buffer(accountKey, 'base64'))
.update(stringToSign, 'utf-8')
.digest('base64');
console.log('--------------------------------------');
console.log(hmac);
request.delete({
'headers': {
'Authorization': 'SharedKeyLite ' + accountName + ':' + hmac,
'x-ms-date': timestamp,
'x-ms-version': '2016-05-31',
'Content-Type': 'application/json',
'If-Match': '*'
},
'url': endpoint,
'json': true
}, function (err, result) {
if (err) {
console.log('inside delete err', JSON.stringify(err));
} else {
console.log(JSON.stringify(result));
}
});
测试结果:
对于更新实体和删除实体rest api,标头If-Match
是必需的(请参阅请求标头)。否则会抛出错误“AuthenticationFailed, xxxx”。
在您的方法中updateEntity
,添加标题部分,例如:'If-Match': '*'
const response = await client.put(`/${tableName}(PartitionKey='${partitionKey}', RowKey='${rowKey}')`, payload,{
headers: {
'x-ms-date': new Date().toUTCString(),
'Authorization': sharedKey.GenerateSharedKeyLite(tableName),
Accept: 'application/json;odata=nometadata',
'x-ms-version': '2015-12-11',
'Content-Type': 'application/json',
'If-Match': '*'
}
推荐阅读
- php - 有没有办法处理重复的输入?
- facebook - 如何将“publish_pages”权限添加到我的 Facebook 应用程序的 test_user?
- sql-server-2017 - 到期余额利息的计算
- google-sheets - 自动计算单元格
- java - 特定模式作为模运算符的输出
- ajax - jquery 自动完成现在工作正常
- oauth-2.0 - 为授权重定向 URI 添加 http://192.168.64.2:8000 域
- python - 有没有办法阻止“unicode escape”错误无法解码字节
- python-3.x - 出现错误 {"detail":"Method \"GET\" not allowed."}
- c - 我想在 UNIX 中制作复制功能文件