You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Azure Blob Storage上传方案遭遇请求认证失败求助

Azure Blob Storage自定义上传鉴权错误排查

问题背景

尝试使用@azure/storage-blob包实现Azure存储上传功能时,与所有Node.js版本(16、18、20等)均存在冲突,持续抛出node:os module not found错误。改用自定义上传方案后,遇到鉴权失败错误:

Error uploading file to Azure Blob Storage: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

已完成的检查

  • 验证Azure账户名、密钥及容器名均正确。
  • 打印请求头和授权字符串,显示内容无误。
  • 尝试使用Content-Type和x-ms-date请求头。
  • 确保账户密钥已正确解码并用于签名生成。
  • 使用了正确的存储端点:https://accountName.blob.core.windows.net/containerName/folder/subfolder/parking.jpg。

自定义实现代码

const fetch = require('node-fetch');
const crypto = require('crypto');
const { sanitize, getMediasBucketName } = require('../../utils');
const logger = require('../../utils/logger');

class AzureBlobStorage {
  constructor() {
    this.accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
    this.accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY;
    this.containerName = process.env.AZURE_CONTAINER_NAME;

    if (!this.accountName || !this.accountKey || !this.containerName) {
      throw new Error('Azure Storage account name, key, or container name is missing.');
    }

    // Set the endpoint URL for the Blob Storage container
    this.endpoint = `https://${this.accountName}.blob.core.windows.net/${this.containerName}`;
  }

  _generateAuthorizationHeader(method, path, headers) {
    const canonicalHeaders = Object.keys(headers)
        .filter(key => key.toLowerCase().startsWith('x-ms-'))
        .sort()
        .map(key => `${key.toLowerCase()}:${headers[key]}`)
        .join('\n');
    const stringToSign = [
      method,
      '',
      headers['Content-Type'] || '',
      headers['x-ms-date'],
      canonicalHeaders,
      `/${this.accountName}/${this.containerName}${path}`
    ].join('\n');

    // Generate the signature using HMAC-SHA256
    const decodedKey = Buffer.from(this.accountKey, 'base64');
    const signature = crypto.createHmac('sha256', decodedKey).update(stringToSign).digest('base64');

    const authorizationHeader = `SharedKey ${this.accountName}:${signature}`;
    return authorizationHeader;
  }

  async storeFileInDirectory({
                               projectName,
                               fileName,
                               contentBody,
                               contentType,
                               mediaType,
                             }) {
    try {

      const safeFileName = sanitize(fileName);
      const filePath = `/${projectName}/${mediaType}/${safeFileName}`;
      const url = `${this.endpoint}${filePath}`;
      const headers = {
        'x-ms-date': new Date().toUTCString(),
        'Content-Type': contentType,
        'Content-Length': contentBody.length,
        'x-ms-blob-type': 'BlockBlob',
      };
      headers['Authorization'] = this._generateAuthorizationHeader('PUT', filePath, headers);

      const response = await fetch(url, {
        method: 'PUT',
        headers: headers,
        body: contentBody,
      });

      if (!response.ok) {
        throw new Error(`Error uploading file to Azure Blob Storage: ${response.statusText}`);
      }

      logger.info(`File uploaded to Azure: ${filePath}`);

      return {
        filePath
      };
    } catch (e) {
      logger.error('Error uploading file to Azure Blob Storage', {
        errorMessage: e.message,
        stack: e.stack,
        params: { projectName, fileName, mediaType },
      });
      return null;
    }
  }
}

问题根源与修复方案

核心问题

自定义签名生成的stringToSign完全不符合Azure SharedKey鉴权规范,主要错误点:

  1. 字段顺序错误:未遵循Azure规定的字段排列逻辑,错误将x-ms-date放在标准Date头的位置,且遗漏了Content-Length等必填字段。
  2. Content-Length未纳入签名计算:该字段是stringToSign的必填组成部分,未加入会直接导致签名不匹配。

修复后的代码

修改_generateAuthorizationHeader方法,严格按照Azure规范构建签名字符串:

const fetch = require('node-fetch');
const crypto = require('crypto');
const { sanitize, getMediasBucketName } = require('../../utils');
const logger = require('../../utils/logger');

class AzureBlobStorage {
  constructor() {
    this.accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
    this.accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY;
    this.containerName = process.env.AZURE_CONTAINER_NAME;

    if (!this.accountName || !this.accountKey || !this.containerName) {
      throw new Error('Azure Storage account name, key, or container name is missing.');
    }

    this.endpoint = `https://${this.accountName}.blob.core.windows.net/${this.containerName}`;
  }

  _generateAuthorizationHeader(method, path, headers) {
    // 规范头处理:转小写、排序、值去除首尾空格
    const canonicalHeaders = Object.keys(headers)
        .filter(key => key.toLowerCase().startsWith('x-ms-'))
        .sort()
        .map(key => `${key.toLowerCase()}:${headers[key].trim()}`)
        .join('\n');
    
    // 严格按照Azure SharedKey规范构建签名字符串
    const stringToSign = [
      method.toUpperCase(), // HTTP方法强制大写
      headers['Content-Encoding'] || '',
      headers['Content-Language'] || '',
      headers['Content-Length'] || '',
      headers['Content-MD5'] || '',
      headers['Content-Type'] || '',
      '', // 使用x-ms-date时,标准Date头留空
      headers['If-Modified-Since'] || '',
      headers['If-Match'] || '',
      headers['If-None-Match'] || '',
      headers['If-Unmodified-Since'] || '',
      headers['Range'] || '',
      canonicalHeaders,
      `/${this.accountName}/${this.containerName}${path}`
    ].join('\n');

    const decodedKey = Buffer.from(this.accountKey, 'base64');
    const signature = crypto.createHmac('sha256', decodedKey).update(stringToSign, 'utf8').digest('base64');

    return `SharedKey ${this.accountName}:${signature}`;
  }

  async storeFileInDirectory({
                               projectName,
                               fileName,
                               contentBody,
                               contentType,
                               mediaType,
                             }) {
    try {
      const safeFileName = sanitize(fileName);
      const filePath = `/${projectName}/${mediaType}/${safeFileName}`;
      const url = `${this.endpoint}${filePath}`;
      const headers = {
        'x-ms-date': new Date().toUTCString(),
        'Content-Type': contentType,
        'Content-Length': contentBody.length.toString(), // 转为字符串避免格式异常
        'x-ms-blob-type': 'BlockBlob',
      };
      headers['Authorization'] = this._generateAuthorizationHeader('PUT', filePath, headers);

      const response = await fetch(url, {
        method: 'PUT',
        headers: headers,
        body: contentBody,
      });

      if (!response.ok) {
        // 获取Azure返回的详细错误信息,便于排查
        const errorDetails = await response.text();
        throw new Error(`Error uploading file to Azure Blob Storage: ${response.statusText}, Details: ${errorDetails}`);
      }

      logger.info(`File uploaded to Azure: ${filePath}`);
      return { filePath };
    } catch (e) {
      logger.error('Error uploading file to Azure Blob Storage', {
        errorMessage: e.message,
        stack: e.stack,
        params: { projectName, fileName, mediaType },
      });
      return null;
    }
  }
}

额外优化点

  1. 获取详细错误信息:请求失败时解析响应文本,Azure会返回具体的鉴权不匹配原因,大幅提升排查效率。
  2. Content-Length转字符串:避免部分环境下数字类型头信息导致的格式异常。
  3. HTTP方法大写:Azure要求签名中的HTTP方法为大写,消除大小写不匹配风险。

官方包问题补充

若后续想重新使用@azure/storage-blob,node:os module not found错误通常由包版本与模块解析方式不兼容导致,可尝试:

  • 升级包到最新稳定版
  • 检查项目是否启用ESM模式,若启用需确保import语法正确,或在package.json中添加type: "commonjs"
  • 删除node_modules和package-lock.json后重新安装依赖

内容的提问来源于stack exchange,提问作者Danis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:53:12