ASP.NET Web API 心得筆記 (8) HTTP 訊息處理常式(HTTP Message Handlers)

ASP.NET Web API 有一個管道(pipeline)在 Client 端與 Server 端之間去處理 HTTP訊息(HTTP messages)。在 Client 端與 Server 端之間被設計成對稱但是獨立,你能只使用 Client 或 Server 來處理。它們兩者都是建立在相同的物件上:

以下都是在 .NET Framework 4.5 Beta 或 System.Net.Http 命名空間(處理 Client 端)或 System.Web.Http 命名空間。

Client 訊息處理

在 Client 端 .NET 應用程式能使用 HttpClient 類別來傳送 HTTP 請求。每一個請求,HttpClient 建立一個 HttpRequestMessage 執行個體。或者,應用程式能建立一個 HttpRequestMessage 直接傳遞至 HttpClient

Client 端 HTTP 訊息處理
圖一:Client 端 HTTP 訊息處理

如果你沒有指定訊息處理常式(message handler),HttpMessageHandler 類別會被當成預設使用。HttpMessageHandler 會轉換 HttpRequestMessage 到一個 HTTP 請求,然後透過網路傳送 HTTP 請求,一樣會轉換 HTTP 回應到一個 HttpResponseMessage 類別

HttpClientHandler 轉換
圖二:HttpClientHandler 轉換

應用程式也能提供自訂訊息處理常式給 HttpClient,或是使用 delegator pattern 來鏈結(chain)數個處理常式。

自訂訊息處理常式
圖三:自訂訊息處理常式

Server 訊息處理

在 Server 端,主機(host)接收 HTTP 請求,並將它們傳遞給從 HttpMessageHandler 而來的 HttpServer 類別,請求會通過數個訊息處理常式,在鏈結最後一個處理常式為 HttpControllerDispatcher,此處理常式使用路由表(routing table)去路由請求至 Web API Controller,Controller 建立回應,然後將回應回傳給訊息處理常式的鏈結。


HttpServer 類別處理流程
圖四:HttpServer 類別處理流程

任何訊息處理常式都能快捷的處理此程序和建立回應,直接繞過其它的鏈結。這樣能讓你在訊息處理管道裡早先去執行驗證(validation)認證(authentication),甚至在訊息到達 controller 之前。

因為 HttpServer 是一個訊息處理常式,你能直接外掛它到 HttpClient 類別中,類似以下的程式碼:

Dim config = New HttpConfiguration()
config.Routes.MapHttpRoute("default",
                           "api/{controller}/{id}",
                           New With {.id = RouteParameter.Optional})

Dim server As HttpServer = New HttpServer(config)

' client 直接連絡到 server
Dim client As HttpClient = New HttpClient(server)

Dim response = client.GetAsync("http://kkbruce.net/api/products").Result

在此範例,HTTP 請求是在記憶體中處理完畢,沒有透過網路。當我們沒有實際管理(hosting)的伺服器,以上情境能提供有用的伺服器管道(pipeline)函式測試。

撰寫自訂訊息處理常式

顧名思義,訊息處理常式(message handlers)是對 HTTP 訊息進行操作。在伺服器端,他們處理該請求,在任何 model 綁定發生之前,它將被路由到 Controller。

若要編寫自訂訊息處理常式,需衍生自 System.Net.Http.DelegatingHandler 類別DelegatingHandler 類別使用授權模式(delegator pattern)。每個 DelegatingHandler 的執行個體都會去參考其他的訊息處理常式,必須呼叫 innerHandler 屬性。這種方式,你可以透過鏈結(chain)方式連結幾個處理常式在一起。

在你的衍生類別,覆寫 SendAsync 方法,此方法有以下的簽章:

Function SendAsync(request As HttpRequestMessage, 
                   cancellationToken As CancellationToken) _ 
                   As Task(Of HttpResponseMessage)

此方法採用 HttpRequestMessage 作為輸入,並以非同步方式返回 HttpResponseMessage。典型的 SendAsync 實作如下:

  1. 處理請求訊息(request message)
  2. 呼叫 MyBase.SendAsync 去傳送訊息到 innerHandler。此步驟是非同步。
  3. 處理回應訊息(response message()和回傳給呼叫者

或者,可跳過步驟 2,並直接建立回應訊息,繞過訊息管道的其餘部分。

範例:支援 X-HTTP-Method-Override

X-HTTP-Method-Override 是個不標準的 HTTP 標頭,它設計用戶端為不能發送如 PUT 或 DELETE 等類型的請求,相反的,用戶端可以發送 HTTP POST 並將  X-HTTP-Method-Override 標頭來設置為所需的方法。例如:


X-HTTP-Method-Override: PUT

如果伺服器支援此標頭,它就會將此請求當成一個 HTTP PUT 請求。

下面範例會顯示一個訊息處理常式如何去加入支援 X-HTTP-Method-Override 標頭

Imports System.Net.Http

''' <summary>
''' 自訂訊息處理常式
''' </summary>
''' <remarks>支援  X-HTTP-Method-Override 標頭</remarks>
Public Class MethodOverrideHandler
    Inherits DelegatingHandler

    ReadOnly _methods() As String = {"DELETE", "HEAD", "PUT"}
    Const _header As String = "X-HTTP-Method-Override"

    ''' <summary>
    ''' 覆寫 SendAsync 方法
    ''' </summary>
    ''' <param name="request">HttpRequestMessage 物件</param>
    ''' <param name="cancellationToken">CancellationToken 物件</param>
    ''' <returns>HttpResponseMessage 物件</returns>
    Protected Overrides Function SendAsync(request As HttpRequestMessage,
                                          cancellationToken As Threading.CancellationToken) _
                                            As Threading.Tasks.Task(Of HttpResponseMessage)

        ' 檢查 HTTP POST 與  X-HTTP-Method-Override
        If request.Method = HttpMethod.Post AndAlso request.Headers.Contains(_header) Then
            Dim method = request.Headers.GetValues(_header).FirstOrDefault()
            If (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase)) Then
                ' 改變請求方法
                request.Method = New HttpMethod(method)
            End If
        End If

        Return MyBase.SendAsync(request, cancellationToken)
    End Function
End Class

SendAsync 方法,處理常式確認請求訊息是否為 POST 請求,也確認是否含有  X-HTTP-Method-Override 標頭。如果有,驗證標頭的值,然後修改請求的方法。然後處理常式呼叫 MyBase.SendAsync 傳遞訊息到下一個處理常式。

當請求到達 HttpControllerDispatcher 類別HttpControllerDispatcher 類別將在已變更的請求方法上路由此請求。

範例:新增自訂 Response 標頭

以下訊息處理常式會新增自己標頭(header)到所有回應訊息中:

Imports System.Net.Http

''' <summary>
''' 新增自訂回應標頭
''' </summary>
Public Class CustomHeaderHandler
    Inherits DelegatingHandler

    ''' <summary>
    ''' 覆寫 SendAsync 函式
    ''' </summary>
    ''' <param name="request">HttpRequestMessage 物件</param>
    ''' <param name="cancellationToken">CancellationToken 物件</param>
    ''' <returns>HttpResponseMessage 物件</returns>
    ''' <remarks>透過 Task.ContinueWith 方法來新增自訂回該標頭</remarks>
    Protected Overrides Function SendAsync(request As HttpRequestMessage,
                                           cancellationToken As Threading.CancellationToken) _
                                           As Threading.Tasks.Task(Of HttpResponseMessage)

        Return MyBase.SendAsync(request, cancellationToken).
            ContinueWith(
                Function(task)
                    Dim response As HttpResponseMessage = task.Result
                    response.Headers.Add("X-Custom-Header", "This is my custom header.")
                    Return response
                End Function)
    End Function
End Class

此處理常式會先呼叫 MyBase.SendAsync 去傳遞請求訊息到內部訊息處理常式(inner message handler)。內部處理常式回傳回應訊息,但它不會去使用非同步的 Task(Of T) 物件。回應訊息直到 MyBase.SendAsync 完成非同前不是有效的。

要修改回應,使用 Task.ContinueWith 方法,它將繼續(continuation)增加到非同步任務中。繼續(continuation)是當前任務完成後接下去執行的任務。

範例:驗證 API Key

有些 Web Services 需要用戶端去包含一組 API Key,以下範例會示範如何在訊息處理常式去驗證 API Key。

Imports System.Net.Http

''' <summary>
''' 驗證 API Key
''' </summary>
Public Class ApiKeyHandler
    Inherits DelegatingHandler

    Property Key As String

    Sub New(key As String)
        Me.Key = key
    End Sub

    ''' <summary>
    ''' 覆寫 SendAsync 函式
    ''' </summary>
    ''' <param name="request">HttpRequestMessage 物件</param>
    ''' <param name="cancellationToken">CancellationToken 物件</param>
    ''' <returns>HttpResponseMessage 物件</returns>
    Protected Overrides Function SendAsync(request As HttpRequestMessage,
                                           cancellationToken As Threading.CancellationToken) _
                                           As Threading.Tasks.Task(Of HttpResponseMessage)

        If Not ValidateKey(request) Then
            Return Threading.Tasks.Task(Of HttpResponseMessage).
                Factory.
                StartNew(Function()
                             Return New HttpResponseMessage(Net.HttpStatusCode.Forbidden)
                         End Function)
        End If

        Return MyBase.SendAsync(request, cancellationToken)
    End Function

    ''' <summary>
    ''' 驗證 API Key
    ''' </summary>
    ''' <param name="message">傳入訊息</param>
    ''' <returns>Boolean</returns>
    ''' <remarks>將訊息參數中的key與物件屬性key進行比對</remarks>
    Private Function ValidateKey(message As HttpRequestMessage) As Boolean
        Dim query = message.RequestUri.ParseQueryString()
        Dim Querykey As String = query("key")
        Return Querykey = Me.Key
    End Function

End Class

此處理常式會在 URI 查詢字串(query string)搜尋 API Key。(此範例中,我們假設 key 是一個固定字串,在直實實作中應該要有更複雜的驗證。)如果 Key 出現在查詢字串中,處理常式會傳遞此請求到內部處理常式進行處理。

不管如何,如果請求沒有包含合法的 Key,處理常式會去建立一個新的 HTTP  403, Forbidden 狀態的回應訊息。處理常式回傳的回應是一個非同步任務,必須呼叫 Task.Factory.StartNew 來替代呼叫 MyBase.SendAsync。這意思是,內部處理常式從未收到請求,而請求也會繞過 Controller。因此,Controller 可以假設所有傳入的請求都有一個有效的 API Key。

注意:如果 API Key 僅適用於某些 Controller,應該考慮使用動作過濾器(action filter)來替代訊息處理常式。動作過濾器會運作在 URI 路由執行之後。

範例:新增自訂訊息處理常式

如果你管理(hosting)一個 ASP.NET 應用程式,可在 Global.asax 檔案的 Application_Start 方法中去定義訊息處理常式。新增一個處理常式到 HttpConfiguration 物件,此物件是靜態 GlobalConfiguration 物件的成員。

' Note: For instructions on enabling IIS6 or IIS7 classic mode, 
' visit http://go.microsoft.com/?LinkId=9394802
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Web.Http
Imports System.Web.Optimization

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    ''' <summary>
    ''' 設定所要增加的自訂訊息處理常式
    ''' </summary>
    ''' <param name="config">HTTP 組態設定物件</param>
    Shared Sub Configure(config As HttpConfiguration)
        config.MessageHandlers.Add(New MethodOverrideHandler())
        config.MessageHandlers.Add(New CustomHeaderHandler())
        config.MessageHandlers.Add(New ApiKeyHandler("secret"))
    End Sub

    Sub Application_Start()
        ' 進行全域組態設定
        Configure(GlobalConfiguration.Configuration)

        AreaRegistration.RegisterAllAreas()

        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters)
        RouteConfig.RegisterRoutes(RouteTable.Routes)
        BundleConfig.RegisterBundles(BundleTable.Bundles)
    End Sub
End Class

以上是 ASP.NET MVC 4 Beta 裡的 Global.asax 檔案,我們新增一個靜態函式 Sub Configure(),增加我們想增加的自訂訊息處理常式。然後到 Application_Start()GlobalConfiguration.Configuration 物件傳入進行設定的動作即可。

在伺服器端,你不需要去設定內部處理常式,Web API Framework 會自動去連接相關訊息處理常式。訊息處理常式會依相反順序去呼叫 MessageHandlers 集合。以上面範例而言,ApiKeyHandler 會先被呼叫,然後是 CustomHeaderHandler,最後是 MethodOverrideHandler。回應訊息會傳播到另一個方向。

如果你是 self-hosting (Web API 的另一種服務形式,以後會介紹),你可以建立一個 HttpSelfHostConfiguration 類別的執行個體,然後新增處理常式到 MessageHandlers 集合裡。

' 需先從 NuGet 安裝 ASP.NET Web API Self Host package
Dim config As New System.Web.Http.SelfHost.HttpSelfHostConfiguration("http://localhost")
config.MessageHandlers.Add(New MethodOverrideHandler())
config.MessageHandlers.Add(New CustomHeaderHandler())
config.MessageHandlers.Add(New ApiKeyHandler("secret"))

在用戶端,傳遞外部訊息處理常式經由 HttpClient 建構式。若要鏈結(chain)多個訊息處理常式,必須設置鏈結(chain)中的每個訊息處理常式的 InnerHandler 屬性。

下面範例示範如何去新增一個自訂處理常式:

Dim handler As New CustomHeaderHandler With {
        .InnerHandler = New HttpClientHandler()
    }

Dim client As New HttpClient(handler)




沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。