首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 顯示站點外目錄/UNC網絡路徑圖片:ASHX 流式代理完整實戰與安全加固

     編輯:ASP.NET 基礎

場景痛點

企業內部系統常需顯示:
📁 本地非站點目錄:`D:\Archives\Images\` `E:\ScanData\`
🌐 映射網絡磁盤:`Z:\Shared\Photos\` (實為 `\\FileServer\Share\Photos\`)
🔗 UNC 路徑:`\\FileServer\Department\Images\`
直接用 `` **瀏覽器安全策略禁止**,必須通過 HTTP 端點代理讀取。

核心實現:ShowOutSiteImage.ashx

<%@ WebHandler Language="C#" Class="ShowOutSiteImage" %>
using System;
using System.IO;
using System.Web;

public class ShowOutSiteImage : IHttpHandler
{
    // 白名單根目錄(配置在 Web.config appSettings 更佳)
    private static readonly string[] AllowedRoots = new[]
    {
        @"D:\",
        @"E:\",
        @"\\FileServer\Share\",
        @"\\BackupServer\Images\"
    };

    public void ProcessRequest(HttpContext context)
    {
        string rawPath = context.Request.QueryString["file"];
        if (string.IsNullOrWhiteSpace(rawPath))
        {
            RespondError(context, 400, "Missing file parameter");
            return;
        }

        // 1. 路徑規範化與安全檢查
        string safePath;
        try
        {
            string fullPath = Path.GetFullPath(rawPath); // 解析 .. 、UNC 規範化
            bool allowed = AllowedRoots.Any(root => 
                fullPath.StartsWith(Path.GetFullPath(root), StringComparison.OrdinalIgnoreCase));
            if (!allowed)
            {
                RespondError(context, 403, "Path not allowed");
                return;
            }
            safePath = fullPath;
        }
        catch (Exception ex)
        {
            RespondError(context, 400, "Invalid path: " + ex.Message);
            return;
        }

        // 2. 文件存在性與副檔名白名單
        if (!File.Exists(safePath))
        {
            RespondError(context, 404, "File not found");
            return;
        }
        string ext = Path.GetExtension(safePath).ToLowerInvariant();
        string[] imgExts = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff" };
        if (!imgExts.Contains(ext))
        {
            RespondError(context, 415, "Unsupported image format");
            return;
        }

        // 3. 高效流式輸出
        context.Response.Clear();
        context.Response.BufferOutput = false; // 關鍵:大圖不緩衝
        context.Response.ContentType = GetMimeType(ext);
        context.Response.AddHeader("Content-Length", new FileInfo(safePath).Length.ToString());
        context.Response.AddHeader("Cache-Control", "public, max-age=2592000"); // 30天
        context.Response.AddHeader("ETag", $"\"{GetFileETag(safePath)}\"");

        // 條件請求支援 (304 Not Modified)
        string ifNoneMatch = context.Request.Headers["If-None-Match"];
        if (!string.IsNullOrEmpty(ifNoneMatch) && ifNoneMatch == $"\"{GetFileETag(safePath)}\"")
        {
            context.Response.StatusCode = 304;
            context.Response.End();
            return;
        }

        try
        {
            using (FileStream fs = File.OpenRead(safePath))
            {
                fs.CopyTo(context.Response.OutputStream);
            }
            context.Response.Flush();
        }
        catch (Exception ex)
        {
            // 記錄日誌,不向前端暴露細節
            System.Diagnostics.Trace.TraceError($"Image proxy error: {ex}");
            context.Response.StatusCode = 500;
        }
    }

    private static string GetMimeType(string ext) => ext switch
    {
        ".jpg" or ".jpeg" => "image/jpeg",
        ".png" => "image/png",
        ".gif" => "image/gif",
        ".bmp" => "image/bmp",
        ".webp" => "image/webp",
        ".tiff" => "image/tiff",
        _ => "application/octet-stream"
    };

    private static string GetFileETag(string path)
    {
        var fi = new FileInfo(path);
        return $"{fi.LastWriteTimeUtc.Ticks:x}-{fi.Length:x}";
    }

    private static void RespondError(HttpContext context, int code, string msg)
    {
        context.Response.StatusCode = code;
        context.Response.ContentType = "text/plain; charset=utf-8";
        context.Response.Write(msg);
        context.Response.End();
    }

    public bool IsReusable => true; // 無狀態,線程安全
}

前端調用方式

<!-- ASP.NET Web Forms -->
<asp:Image ID="Image1" runat="server" 
    ImageUrl='<%# "ShowOutSiteImage.ashx?file=" + Server.UrlEncode(Eval("PhysicalPath").ToString()) %>' />

<!-- MVC / Razor -->
<img src="@Url.Content("~/ShowOutSiteImage.ashx?file=" + Url.Encode(Model.PhysicalPath))" />

<!-- 純 HTML/JS 動態 -->
<script>
function loadImage(elementId, physicalPath) {
    document.getElementById(elementId).src = 
        "ShowOutSiteImage.ashx?file=" + encodeURIComponent(physicalPath);
}
</script>

UNC 路徑訪問權限配置(關鍵)

  1. 應用程式池身份:IIS 管理器 → 應用程式池 → 進階設定 → 標識 → **自定義賬戶** → 輸入有網絡共享**讀取權限**的域用戶(如 `CORP\IIS_ImageReader`)。
  2. 共享權限:文件服務器上,共享資料夾 → 權限 → 添加上述用戶 → **讀取**。
  3. NTFS 權限:資料夾屬性 → 安全 → 添加用戶 → **讀取/列出資料夾內容**。
  4. 防火牆:確保 Web 服務器能通過 445 端口訪問文件服務器。

進階優化:記憶體緩存熱門圖片

// 靜態緩存字典(生產建議用 MemoryCache / Redis)
private static readonly System.Collections.Concurrent.ConcurrentDictionary _cache 
    = new System.Collections.Concurrent.ConcurrentDictionary();

// 在 ProcessRequest 開頭添加
if (_cache.TryGetValue(safePath, out byte[] cached))
{
    context.Response.Clear();
    context.Response.ContentType = GetMimeType(ext);
    context.Response.BinaryWrite(cached);
    context.Response.End();
    return;
}

// 輸出流結束前緩存(限制大小,如 < 500KB)
if (new FileInfo(safePath).Length < 500 * 1024)
{
    using (var ms = new MemoryStream())
    {
        using (var fs = File.OpenRead(safePath)) fs.CopyTo(ms);
        _cache.TryAdd(safePath, ms.ToArray());
    }
}

安全加固清單

風險對策
路徑遍歷 (../../web.config)`Path.GetFullPath` + 白名單前綴嚴格匹配
任意文件讀取 (C:\Windows\system32\drivers\etc\hosts)副檔名白名單 + 只允許圖片 MIME
目錄遍歷洩露結構不提供列表接口,僅單文件讀取
大文件 DoS (佔用記憶體/帶寬)`BufferOutput=false` + 流式複製 + 速率限制 (IP/Token)
UNC 憑證洩露應用程式池專用低權限賬戶,不嵌入代碼

總結

| 方案 | 適用性 | 性能 | 安全性 | 維護性 |
|---|---|---|---|---|
| ASHX 代理 + 白名單 + 流式 | ✅ 通用、權限控制、UNC 支援 | ⭐⭐⭐⭐ (流式、可緩存、304) | ⭐⭐⭐⭐⭐ (多層防護) | ⭐⭐⭐⭐ (單文件、易擴展) |
| IIS 虛擬目錄直連 | 僅公開靜態資源 | ⭐⭐⭐⭐⭐ (內核傳輸) | ⭐ (無動態權限) | ⭐⭐⭐ (需 IIS 配置) |
| 資料庫存 Blob | 少量、強一致性 | ⭐⭐ (DB 壓力大) | ⭐⭐⭐⭐ | ⭐⭐ (備份/遷移複雜) |

**企業級推薦**:ASHX 代理作為統一圖片訪問網關,配合白名單、流式輸出、ETag 緩存、專用服務賬戶,滿足安全、性能、維護三重需求。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved