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

配置vNet集成后Azure存储GetBlob失败,Put/ListBlob正常求助

问题:Azure存储账户GetBlob请求403错误(vNet集成+服务端点配置场景)

我为Python应用配置了vNet集成,同时为存储账户配置了Service Endpoint并启用了vNet防火墙。PutBlob和ListBlob请求可成功执行,但GetBlob请求失败,无法在测试应用中查看图片。

存储日志详情

2.0;2024-07-11T11:32:44.8196257Z;ListBlobs;Success;200;9;6;authenticated;storagecasbx01we;storagecasbx01we;blob;"https:///<endpoint>.blob.core.windows.net:443/cp-images?restype=container&amp;comp=list";"/<endpoint>/cp-images";67d5b517-701e-002a-5086-d3f759000000;0;10.0.0.254:52420;2020-06-12;575;0;220;2956;0;;;;;"azsdk-python-storage-blob/12.8.1 Python/3.12.2 (Linux-5.15.153.1-2.cm2-x86_64-with-glibc2.31)";;"4a9a9e1e-3f79-11ef-b188-a665be233668";;;;;;;;;
2.0;2024-07-11T11:32:51.9590052Z;PutBlob;BlobAlreadyExists;409;5;5;authenticated;storagecasbx01we;storagecasbx01we;blob;"https://<endpoint>.blob.core.windows.net:443/cp-images/NewSmall.png";"/<endpoint>/cp-images/NewSmall.png";67d5cab5-701e-002a-5f86-d3f759000000;0;10.0.0.254:52420;2020-06-12;659;3966;235;220;3966;;"FUj7LV1B0HHctq3t5gmWCA== ";;;;"If-None-Match=*";"azsdk-python-storage-blob/12.8.1 Python/3.12.2 (Linux-5.15.153.1-2.cm2-x86_64-with-glibc2.31)";;"4eeaa860-3f79-11ef-b188-a665be233668";;;;;;;;;
2.0;2024-07-11T11:32:52.1686959Z;ListBlobs;Success;200;6;3;authenticated;storagecasbx01we;storagecasbx01we;blob;"https:///<endpoint>.blob.core.windows.net:443/cp-images?restype=container&amp;comp=list";"/<endpoint>/cp-images";67d5cb4f-701e-002a-6786-d3f759000000;0;10.0.0.254:52420;2020-06-12;575;0;220;2956;0;;;;;"azsdk-python-storage-blob/12.8.1 Python/3.12.2 (Linux-5.15.153.1-2.cm2-x86_64-with-glibc2.31)";;"4f061460-3f79-11ef-b188-a665be233668";;;;;;;;;
2.0;2024-07-11T11:33:00.5570855Z;ListBlobs;Success;200;7;2;authenticated;storagecasbx01we;storagecasbx01we;blob;"https:///<endpoint>.blob.core.windows.net:443/cp-images?restype=container&amp;comp=list";"/storagecasbx01we/cp-images";67d5e450-701e-002a-2286-d3f759000000;0;10.0.0.254:52420;2020-06-12;575;0;220;2956;0;;;;;"azsdk-python-storage-blob/12.8.1 Python/3.12.2 (Linux-5.15.153.1-2.cm2-x86_64-with-glibc2.31)";;"540c6748-3f79-11ef-b188-a665be233668";;;;;;;;;
2.0;2024-07-11T11:32:45.3248984Z;GetBlob;AnonymousIpAuthorizationError;403;4;4;anonymous;;storagecasbx01we;blob;"https:///<endpoint>.blob.core.windows.net:443/cp-images/images.png";"/";1392860f-f01e-000b-4186-d3d322000000;0;163.116.166.103:8762;2009-09-19;624;0;105;246;0;;;;;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";"https://<app_service>.azurewebsites.net/";;;;;;;;;;
2.0;2024-07-

Python代码片段

@app.route("/")
def view_photos():
    blob_items = container_client.list_blobs() # list all the blobs in the container

    img_html = "<div style='display: flex; justify-content: space-between; flex-wrap: wrap;'>"

    for blob in blob_items:
        blob_client = container_client.get_blob_client(blob=blob.name) # get blob client to interact with the blob and get blob url
        img_html += "<img src='{}' width='auto' height='200' style='margin: 0.5em 0;'/>".format(blob_client.url) # get the blob url and append it to the html
    
    img_html += "</div>"

    # return the html with the images
    return """
    <head>
    <!-- CSS only -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <div class="container">
                <a class="navbar-brand" href="/">Photos App</a>
            </div>
        </nav>
        <div class="container">
            <div class="card" style="margin: 1em 0; padding: 1em 0 0 0; align-items: center;">
                <h3>Upload new File</h3>
                <div class="form-group">
                    <form method="post" action="/upload-photos" 
                        enctype="multipart/form-data">
                        <div style="display: flex;">
                            <input type="file" accept=".png, .jpeg, .jpg, .gif" name="photos" multiple class="form-control" style="margin-right: 1em;">
                            <input type="submit" class="btn btn-primary">
                        </div>
                    </form>
                </div> 
            </div>
        
    """ + img_html + "</div></body>"

问题分析

从日志可见:

  • PutBlob和ListBlob请求来自App Service的私有IP(10.0.0.254),且已通过认证,能正常访问存储账户
  • GetBlob请求来自客户端浏览器的公网IP(163.116.166.103),属于匿名访问,被存储账户的vNet防火墙拦截,触发AnonymousIpAuthorizationError导致403

根本原因是当前代码直接将blob的原始URL返回给前端,浏览器会绕过App Service,直接向存储账户发起请求,无法利用App Service的vNet集成通道。

解决方案

选项1:生成SAS令牌(推荐)

为每个blob生成带有效期的SAS令牌,将包含SAS的URL返回给前端。这样浏览器即使从公网发起请求,也能通过SAS令牌完成认证,无需修改防火墙配置。

修改代码中的blob URL生成逻辑:

from azure.storage.blob import generate_blob_sas, BlobSasPermissions
from datetime import datetime, timedelta

@app.route("/")
def view_photos():
    blob_items = container_client.list_blobs() 
    img_html = "<div style='display: flex; justify-content: space-between; flex-wrap: wrap;'>"

    for blob in blob_items:
        blob_client = container_client.get_blob_client(blob=blob.name)
        # 生成SAS令牌,有效期1小时,仅授予读权限
        sas_token = generate_blob_sas(
            account_name=container_client.account_name,
            container_name=container_client.container_name,
            blob_name=blob.name,
            account_key=container_client.credential.account_key,
            permission=BlobSasPermissions(read=True),
            expiry=datetime.utcnow() + timedelta(hours=1)
        )
        blob_url_with_sas = f"{blob_client.url}?{sas_token}"
        img_html += "<img src='{}' width='auto' height='200' style='margin: 0.5em 0;'/>".format(blob_url_with_sas)
    
    img_html += "</div>"
    # 其余代码保持不变

选项2:通过App Service代理Blob请求

新增一个路由,让App Service作为中间层,从存储账户获取blob内容后返回给前端。所有请求都走App Service的vNet集成通道,保持存储账户的私有访问特性。

示例代码:

@app.route("/blob/<path:blob_name>")
def serve_blob(blob_name):
    blob_client = container_client.get_blob_client(blob=blob_name)
    blob_data = blob_client.download_blob().readall()
    # 根据文件扩展名动态设置Content-Type
    content_type = "image/png"
    if blob_name.endswith(".jpg") or blob_name.endswith(".jpeg"):
        content_type = "image/jpeg"
    elif blob_name.endswith(".gif"):
        content_type = "image/gif"
    return blob_data, 200, {"Content-Type": content_type}

# 修改view_photos中的img标签src为代理路由
@app.route("/")
def view_photos():
    blob_items = container_client.list_blobs() 
    img_html = "<div style='display: flex; justify-content: space-between; flex-wrap: wrap;'>"

    for blob in blob_items:
        img_html += "<img src='/blob/{}' width='auto' height='200' style='margin: 0.5em 0;'/>".format(blob.name)
    
    img_html += "</div>"
    # 其余代码保持不变

选项3:允许客户端公网IP(不推荐)

在存储账户的防火墙规则中添加客户端的公网IP地址。此方法会降低存储账户的安全性,且客户端IP变化时需要手动更新防火墙配置,仅适合临时测试场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 06:30:54