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

ASP.NET 攔截輸出流三大法:Response.Filter、HttpModule、反射替換 _writer 完整實戰

     編輯:ASP.NET 基礎

前言

有時需在頁面輸出到瀏覽器前,統一修改 HTML(如注入腳本、關鍵字替換、壓縮、加簽章)。ASP.NET 提供多種攔截點:Response.Filter、HttpModule 事件、甚至反射替換內部 _writer 欄位。本文詳細實作三種方案並比較優劣。

核心原理

HttpResponse.Output 屬性為 TextWriter,內部實際寫入的是私有欄位 _writer。若能替換此欄位為自訂 StringWriter,即可捕獲所有 Response.Write、控制項 Render 輸出的內容,處理完再寫回原輸出流。

方法一:頁面層級反射替換 _writer(最直接、侵入性最小)

public partial class _Default : System.Web.UI.Page
{
    StringBuilder content = new StringBuilder();
    TextWriter tw_old, tw_new;
    FieldInfo tw_field;

    protected void Page_Load(object sender, EventArgs e)
    {
        var context = HttpContext.Current;
        tw_old = context.Response.Output; // 原始輸出
        tw_new = new StringWriter(content); // 暫存緩衝
        
        var type_rp = context.Response.GetType();
        // 取得私有欄位 _writer
        tw_field = type_rp.GetField("_writer", 
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        tw_field.SetValue(context.Response, tw_new); // 替換
    }

    protected override void Render(HtmlTextWriter writer)
    {
        base.Render(writer); // 此時輸出寫入 tw_new (StringWriter)
        
        // 還原原始輸出
        tw_field.SetValue(HttpContext.Current.Response, tw_old);
        
        // 自訂處理:注入註解
        content.AppendLine("<!-- 江湖小子 -->");
        
        // 寫回真實輸出
        HttpContext.Current.Response.Write(content.ToString());
    }
}

適用: 單一頁面特殊處理、快速原型。
缺點: 依賴私有欄位名稱(_writer),跨 .NET 版本可能失效;每頁需重複程式碼。

方法二:HttpModule 掛在 PreRequestHandlerExecute / Unload(全域、生命週期完整)

public class OutputCaptureModule : IHttpModule
{
    private HttpApplication _app;
    private TextWriter _twOld, _twNew;
    private StringBuilder _content;
    private FieldInfo _writerField;

    public void Init(HttpApplication context)
    {
        _app = context;
        _app.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
    }

    void OnPreRequestHandlerExecute(object sender, EventArgs e)
    {
        var context = _app.Context;
        var page = context.Handler as Page;
        if (page == null) return; // 非頁面請求(如 WebService、Handler)略過
        
        page.Unload += OnPageUnload; // 頁面渲染完成後觸發
        
        _content = new StringBuilder();
        _twOld = context.Response.Output;
        _twNew = new StringWriter(_content);
        
        var type = context.Response.GetType();
        _writerField = type.GetField("_writer", 
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        _writerField.SetValue(context.Response, _twNew);
    }

    void OnPageUnload(object sender, EventArgs e)
    {
        // 還原
        _writerField.SetValue(HttpContext.Current.Response, _twOld);
        
        // 處理內容
        _content.AppendLine("<!-- 江湖小子 -->");
        HttpContext.Current.Response.Write(_content.ToString());
    }

    public void Dispose() { _app = null; }
}

註冊於 web.config:

<system.webServer>
  <modules>
    <add name="OutputCapture" type="Namespace.OutputCaptureModule" />
  </modules>
</system.webServer>

優點: 全域生效、不修改頁面程式碼、可針對特定路徑/副檔名攔截。
缺點: 同樣依賴反射私有欄位;Unload 事件較晚,若前面已 Response.End() 可能不觸發。

方法三:HttpModule 掛在 BeginRequest / EndRequest(更早攔截、無需反射改用 Response.Filter)

此法改用 Response.Filter 屬性(Stream),避開反射,相容性更好:

public class StreamFilterModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += (s, e) => 
        {
            var ctx = ((HttpApplication)s).Context;
            ctx.Response.Filter = new MemoryStream(); // 替換為記憶體流
        };
        
        app.EndRequest += (s, e) => 
        {
            var ctx = ((HttpApplication)s).Context;
            var filter = ctx.Response.Filter as MemoryStream;
            if (filter == null) return;
            
            filter.Position = 0;
            var html = new StreamReader(filter, ctx.Response.ContentEncoding).ReadToEnd();
            
            // 處理 HTML
            html += "<!-- jhxz -->";
            
            // 寫回原始流(需先清空 Filter)
            ctx.Response.Filter = null; 
            ctx.Response.Write(html);
        };
    }
    public void Dispose() { }
}

注意: Response.Filter 僅攔截 Response.Write/Response.BinaryWrite,控制項 Render 直接寫入 HtmlTextWriter(繞過 Filter)可能漏捕。若需完整捕獲,仍需反射替換 _writer 或使用 HttpResponse.Filter 搭配 Page.Render 重寫。

現代最佳實踐:ASP.NET Core Middleware / Response Rewriting

.NET Core 時代請使用 Middleware:

public class HtmlInjectionMiddleware
{
    private readonly RequestDelegate _next;
    public HtmlInjectionMiddleware(RequestDelegate next) => _next = next;
    
    public async Task InvokeAsync(HttpContext context)
    {
        var originalBody = context.Response.Body;
        using (var memStream = new MemoryStream())
        {
            context.Response.Body = memStream;
            await _next(context);
            
            memStream.Position = 0;
            var html = await new StreamReader(memStream).ReadToEndAsync();
            html += "<!-- Injected by Middleware -->";
            
            context.Response.Body = originalBody;
            await context.Response.WriteAsync(html);
        }
    }
}

// Startup.cs
app.UseMiddleware();

總結比較

方案範圍相容性效能維護性
頁面反射 _writer單頁易失效(私有欄位)高低(重複代碼)
HttpModule + 反射全域易失效高中
HttpModule + Response.Filter全域良好中高
ASP.NET Core Middleware全域最佳高最高

建議

  • 舊專案快速修改:頁面反射法。
  • 全域統一處理(如 SEO 注入、版權聲明):HttpModule + Response.Filter 或升級至 ASP.NET Core Middleware。
  • 需完整捕獲控制項 Render 輸出:反射替換 _writer 或重寫 Page.Render。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved