xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

WCF で認証が必要な Proxy を利用する [.NET Framework / .NET Core / .NET Standard 完全対応]

gRPC という超クールなものが大人気な昨今、WCF なんてひと昔もふた昔も前のもの...と鼻で笑っているあなた!古き良き (?) WCF はまだまだ現役の世界線もあるのですよ!

背景 / 前話

ということで僕は仕事で EC サービスの開発/運営をやっているのですが、とある WCF を採用している外部 API と連携する必要が業務上の必須要件としてあるのです。そして、その外部 API はアクセス元を IP 制限しています。僕たちが運用している EC サービスは Azure Web Apps にホストされているので、IP を固定するためにプロキシサーバーを経由するようにしています。そしてこのプロキシサーバーはいわゆる「ユーザー名 / パスワード」での認証が必要になっています。誰でも自由に踏み台にできてしまったら困りますからね。

...と、ここまではかなりオーソドックスなアプローチかと思うのですが、認証があることによって WCF はメチャクチャ面倒が増えます。そういった部分に対応していくための方法をご紹介します。

Case.1 : 認証なし [.NET Core / .NET Framework]

認証がない場合は非常にシンプルです。Binding インスタンスにこれだけ設定すれば OK です。内部的に WebProxy インスタンスを生成して利用してくれます。

var binding = new BasicHttpsBinding();
binding.ProxyAddress = new Uri($"http://{host}:{port}");
binding.UseDefaultWebProxy = false;

Case.2 : 認証あり - グローバル対応 [.NET Framework]

Case.1 の書き方は簡単なのですが認証情報を設定する方法がありません。ここで一気に躓くのですが、WCF が内部で WebRequest 型を利用して通信しているのを利用すれば以下のように書くことができます。

// WebRequest による通信をプロキシ経由にしてしまおう!(雑
var proxy = new WebProxy(host, port);
proxy.Credentials = new NetworkCredential("UserName", "Password");
WebRequest.DefaultWebProxy = proxy;

var binding = new BasicHttpsBinding();
binding.UseDefaultWebProxy = false;  // システム既定のプロキシを使うかどうかの設定なので false

これはこれで OK なのですが、WebRequest の既定値を変更しているので大半がプロキシを経由することになってしまうでしょう。それが許されるなら良いのですが、そんな雑過ぎる暴挙は本来良くない!

しかもこれは .NET Framework でのみ有効で .NET Core では動きません。「なんでやねん!」と思うかもしれないのですが、.NET Core 版の WCFWebRequest ベースから HttpClient ベースに実装が書き換わっているからです。一筋縄では行かなさそうな嫌な空気感が徐々に出てきましたね。

Case.3 : 認証あり - 個別対応 [.NET Framework]

Case.2 だと WebRequest を利用した他の通信にまでグローバルに影響を与えてしまうのがイヤなので、なんとかして WCF の通信だけにプロキシを適用したいものです。と、ここで意地になって .NET Framework の実装を Reference Source で読み漁ってみると...?なんと裏口入学的に Proxy を差す口があるんですねー。

// こんな独自の Binding 型を用意する
public class WebProxyBinding : BasicHttpsBinding
{
    private WebProxy Proxy { get; }

    public WebProxyBinding(WebProxy proxy)
    {
        this.Proxy = proxy;
        this.UseDefaultWebProxy = false;
    }

    public override BindingElementCollection CreateBindingElements()
    {
        // リフレクションを利用して無理やり internal な 'Proxy' プロパティに WebProxy を差し込む
        // 'ProxyAddress' プロパティなどよりも優先される隠しプロパティ
        var elements = base.CreateBindingElements();
        var element = elements.Find<HttpsTransportBindingElement>();
        var flags = BindingFlags.Instance | BindingFlags.NonPublic;
        var propertyInfo = element.GetType().GetProperty("Proxy", flags);
        if (propertyInfo != null)
            propertyInfo.SetValue(element, this.Proxy);

        return elements;
    }
}
// BasicHttpsBinding の代わりに独自型を利用
var binding = new WebProxyBinding();

internal なプロパティに無理やり差し込むという完全な力業で神回避!最初から Proxy プロパティを公開していてくれれば何も困ることはなかったのに...、と思わなくはないのですが、何か意図があったんでしょうね。ちなみに CreateBindingElements メソッドは WCF のライブラリ側から相当回数コールされます。リフレクションを使っている手前、かなり実行パフォーマンスが低下することが予想されるのでご注意ください。

また、このアプローチは .NET Framework だけで有効です。.NET Core 版の WCF 実装は Proxy プロパティが丸ごと消え去っているので、リフレクションですら手出しができません。上記の実装例だと .NET Core では propertyInfonull で返ってきます。なので、過信してそのまま .NET Framework を .NET Core に移植するとハマります。

Case.4 : 認証あり [.NET Core]

.NET Core で Case.2 も Case.3 も利用できないとなると、.NET Core で WCF + 認証 Proxy を利用している方は本気で頭を抱えると思います。僕もそうでした。とは言え業務。そう易々と諦めることはできません。なんとかしなければと意地になって GitHub にある WCF の .NET Core 実装を読みまくりました。その結果、隠し機能のような差し込み口を発見しました。

// こんな感じの型を用意
public class WebProxyBehavior : IEndpointBehavior
{
    private WebProxy Proxy { get; }

    public WebProxyBehavior(WebProxy proxy)
        => this.Proxy = proxy;

    #region IEndpointBehavior implementations
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        // このデリゲート型 (固定) を突っ込むとコールバックしてくれる!(知らんがな
        // ちょっとでもパフォーマンスを求めるならデリゲートをキャッシュすると善き
        Func<HttpClientHandler, HttpMessageHandler> callback = handler =>
        {
            // ここで設定すると内部で通信する HttpClient に渡される
            handler.Proxy = this.Proxy;
            return handler;
        };
        bindingParameters.Add(callback);
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    { }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    { }

    public void Validate(ServiceEndpoint endpoint)
    { }
    #endregion
}
// プロキシを作って
var proxy = new WebProxy(host, port);
proxy.Credentials = new NetworkCredential("UserName", "Password");
var behavior = new WebProxyBehavior(proxy);

// 差し込む
var binding = new BasicHttpsBinding();
var endpoint = "https://example.com/foo.svc";
var client = new SampleClient(binding, endpoint);
client.Endpoint.EndpointBehaviors.Add(behavior); 

IEndpointBehavior を実装した型で Func<HttpClientHandler, HttpMessageHandler> デリゲートを BindingParameterCollection に突っ込むという隠し機能!見つけたときはメチャメチャ歓喜しましたw ちなみにこのあたりに実装があります。

この拡張ポイントは WCF が通信をするときに内部で利用する HttpClientHttpMessageHandler をカスタマイズするためにあります。今回は Proxy を設定するために利用していますが、通信にかかった時間を測定したりログを出力するなど、通信の前後をフックしていろんなことができるようになっています。全然まともにドキュメント化されていないのが残念ですが、.NET Framework 時代よりも汎用性の高い拡張ポイントを提供していると思います。

ちなみに、この拡張ポイントは .NET Core で新たに追加されたものなので .NET Framework では動作しません。具体的には bindingParameters.Add(callback) でコールバックを登録していてもコールバックされません。無視されます。

Case.5 : 認証あり [.NET Standard]

最後に .NET Standard で WCF の通信をラップするようなライブラリを作る場合です。ここまで読んできてお分かりかと思いますが、 Case.3 と Case.4 を組み合わせれば OK です。お互いの実装がちょうどよく無視されるので、特に干渉することなく動作します。めでたし。