モバイルサービスのASP.NET CoreおよびDockerへの移行について引き続き説明します。 この記事では、前の記事で説明したWCFクライアントモジュール、NTLM認証、および移行中のその他の問題について説明します。 ここで、解剖学を少し勉強し、内部から.NET Coreに触れる必要がある理由を説明します。
ソフトな方法。 Windowsコンテナー
まず、Dockerイメージでデバッグを構成し、Windowsコンテナーでローカルにサービスを開始しました。
WCFサービスに要求を送信しようとすると、非常に華やかなエラーが発生しました。
System.ServiceModel.Security.MessageSecurityException: The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate TlRMTVNTUAACAAAAEAAQADgAA...
トライアルにより、サービスのクレジットにはドメインが必要であるという結論に達しました。 nullでない場合に限り、任意の値を指定できるのは面白いです。
static partial void ConfigureEndpoint(ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials) { ... clientCredentials.Windows.ClientCredential.Domain = ""; }
それだけです、今ではリクエストが送られ、今ではWindowsコンテナでは大丈夫です。 さらに進んでいます。
Linuxで.NET Coreを試す
Linuxコンテナのアセンブリに切り替えると、興味を引くために、Domain値が削除され、機能します。
WCFにリクエストを送信する際の最初の問題はSSLです。 このような誓い:
System.Net.Http.CurlException SSL peer certificate or SSH remote key was not OK
つまり、証明書に対する信頼はありません。 WCFサービスが最終証明書だけでなく、すべての中間証明書も送信した場合、問題はありません。
決定通り:
1.中間証明書を送り出します。
たとえば、Chromeでリンクを開き、[セキュリティ]タブのF12に移動します。 次に証明書を表示→証明書パス。 各証明書について、[証明書の表示]を開き、[ファイルにコピー]ボタンを使用して[詳細]タブで、Base-64でエンコードされた証明書をプロジェクトディレクトリに保存します。 ファイル拡張子を.crtに変更する必要があります。
2. Dockerfileに新しいレイヤーを追加します。
FROM microsoft/aspnetcore:latest AS base WORKDIR /app EXPOSE 80 FROM base AS cert COPY Certificates/*.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates || exit 0
デバッグと実験のために、SSL検証を一時的に無効にすることができます。
clientCredentials.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication() CertificateValidationMode = X509CertificateValidationMode.None, RevocationMode = X509RevocationMode.NoCheck };
CurlExceptionを受け取ったときに学んだ最も有用で重要なことは、libcurlがネットワーク要求に使用されることです。
おいしい部分が先に私たちを待っていました。
Linux + WCF + NTLM =愛、しかし夕食後
今、そのような障害は道路をブロックしています
MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate, NTLM'.
Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows
をHttpClientCredentialType.Ntlm
変更しHttpClientCredentialType.Ntlm
エラーは少し変わりましたが、簡単にはなりません。
MessageSecurityException The HTTP request is unauthorized with client authentication scheme 'Ntlm'. The authentication header received from the server was 'Negotiate, NTLM'.
Domain値のような別のドッカー関連機能に直面していないことを確認しましょう。
Ubuntu LTSを使用して仮想マシンでサービスを開始します。
叙情的な余談:Docker for WindowsはHyper-Vを愛しているため、他の仮想マシンをインストールすると動作しなくなる可能性があります。 そのため、今回はホストとゲストマシン間のコピーペーストが機能しないHyper-VでUbuntuを上げる必要がありました。これは朗報です。
ところで、マイクロソフト、アップルとの友情は何ですか?
その隣には、Visual StudioがインストールされたMacがありました。 手はくしでした。 無効なSslCertificateAuthenticationで開始すると、The handler does not support custom handling of certificates with this combination of libcurl (7.54.0) and its SSL backend ("SecureTransport")
。 証明書の検証をその場所に返すと、NTLMでも同じエラーが発生します。 それでも、最初のエラーは、Linuxとの違いが大きいことを示唆しています。
歪ませる他の方法は何ですか?
Windows上のSystem.DllNotFoundException: Unable to load DLL 'System.Net.Http.Native'
サービスの開始時に、System.DllNotFoundException: Unable to load DLL 'System.Net.Http.Native'
エラーSystem.DllNotFoundException: Unable to load DLL 'System.Net.Http.Native'
。
場合:純粋なLinuxでは、エラーはコンテナのエラーとまったく同じです。つまり、問題は皮肉なことにWCFクライアントの実装にあります。
反対側から入ります。 コマンドラインから次を実行します:
1. curl -v --negotiate
失敗します
* gss_init_sec_context() failed: SPNEGO cannot find mechanisms to negotiate.
2. curl -v --ntlm
すべてが正常で、リクエストは機能します
WCFで公式にサポートされている機能のリストを思い出してください。 検索行では、Linux上のCoreはNTLMの使用方法を知らないと述べています。 しかし、これはcurlができるという事実に固執せず、そのような一般的なオプションを実装しないのは奇妙です。
インターネットに関するコメントから、NegotiateはNegotiate-discordであることがわかります。一部の実装では、フォールバックKerberos→NTLM(Windowsのどこでも)がサポートされますが、そうでないものもあります。 後者のカール、およびネゴシエートは障害になります。
これはすべて、HttpClientがこのニュアンスを考慮に入れていない可能性があることを示唆しています。つまり、勝利への希望があるということです。
ソースコードを見る
そして、ここでは、コードを世界に公開するという新しいマイクロソフトの決定を喜んでいます。 ソートでは、キーCURLHANDLER_DEBUG_VERBOSE=true
見つかりCURLHANDLER_DEBUG_VERBOSE=true
。これにより、WCFクエリの実行時にlibcurlが何を行っているかがわかります。
ログには、HttpClientCredentialType.WindowsとHttpClientCredentialType.Ntlmの両方で既によく知られているエラーgss_init_sec_context() failed
したことがわかります。
これで、WCFクライアントがWindows認証からNTLMへの切り替えに応答せず、両方のケースでNegotiateを使用しようとすることが明らかになりました。 ほとんどの場合、これはWCFサービスを送信するダブルヘッダーWWW-Authentication 'Negotiate、NTLM'によるものであり、Negotiateはより強力な承認であるため、使用されます。
libcurlマニュアルから、許可タイプはCURLOPT_HTTPAUTH
オプションを介して設定されることを味わった。 このトレースに続いて、 許可選択テーブルに行きました:
private static readonly KeyValuePair<string,CURLAUTH>[] s_orderedAuthTypes = new KeyValuePair<string, CURLAUTH>[] { new KeyValuePair<string,CURLAUTH>("Negotiate", CURLAUTH.Negotiate), new KeyValuePair<string,CURLAUTH>("NTLM", CURLAUTH.NTLM), new KeyValuePair<string,CURLAUTH>("Digest", CURLAUTH.Digest), new KeyValuePair<string,CURLAUTH>("Basic", CURLAUTH.Basic), };
static readonly
属性は、特に魅力的です。これは、サービスの開始時にテーブルの値でReflectionをプレイするだけで十分であり、HTTP要求のオーバーヘッドがないことを意味します。
Program.cs
次のコードを追加しました。
public static void Main(string[] args) { … // redirect Negotiate to NTLM (only for libcurl on Linux) var curlHandlerType = typeof(HttpClient).Assembly.GetTypes() .FirstOrDefault(type => type.Name == "CurlHandler"); if (curlHandlerType != null) { var authTypesField = urlHandlerType.GetField("s_orderedAuthTypes", BindingFlags.Static | BindingFlags.NonPublic); var authTypes = authTypesField.GetValue(null); var authTypesGetByIndex = authTypes.GetType().GetMethod("Get"); var ntlmKeyValuePair = authTypesGetByIndex.Invoke(authTypes, new object[] { 1 }); var ntlmValue = ntlmKeyValuePair.GetType().GetProperty("Value"); var CURLAUTH = ntlmValue.GetMethod.ReturnType; var CURLAUTH_NTLM = ntlmValue.GetValue(ntlmKeyValuePair); var authTypeKeyValuePairBuilder = typeof(KeyValuePairBuilder<,>) .MakeGenericType(new[] { typeof(string), CURLAUTH }); var builder = Activator.CreateInstance(authTypeKeyValuePairBuilder); var negotiateToNtlmKeyValuePair = authTypeKeyValuePairBuilder .GetMethod("Build") .Invoke(builder, new object[] { "", CURLAUTH_NTLM }); var authTypesSetByIndex = authTypes.GetType().GetMethod("Set"); authTypesSetByIndex.Invoke(authTypes, new object[] { 0, negotiateToNtlmKeyValuePair }); } } // makes it possible to call Activator.CreateInstance on KeyValuePair struct public class KeyValuePairBuilder<K, V> { public KeyValuePair<K, V> Build(K k, V v) { return new KeyValuePair<K, V>(k, v); } }
ここでは、「ネゴシエート」とCURLAUTH.NTLMの間の対応を確認します。
これで、リクエストは成功しました。
ボーナストラック
私たちはそこで止まりませんでした。 ログを注意深く見ると、1つのWCF要求/応答に複数のHTTP応答要求が含まれており、応答の1つがBad Request
安定して返されていることがわかります。 元気?
誤った要求の場合、 HEAD
メソッドが使用されます。 実際、同じ動作はcurl -I
簡単にエミュレートされます。 libcurlでは、これはCURLOPTION_NOBODY
オプションに対応します。 corefxでは、 HttpMethod.Headリクエストを送信するときにこのオプションが使用されます。
WCFでスタックを上に移動します。 SendPreauthenticationHeadRequestIfNeeded
メソッドでは、承認のためにHEADリクエストが送信され、すべてのエラーが単に無視されることがわかります。
try { // There is a possibility that a HEAD pre-auth request might fail when the actual request // will succeed. For example, when the web service refuses HEAD requests. We don't want // to fail the actual request because of some subtlety which causes the HEAD request. await SendPreauthenticationHeadRequestIfNeeded(); } catch { /* ignored */ }
HttpClientHandler.PreAuthenticate
似たフラグは、事前に400に運命づけられている要求をトリガーしないように、明らかにそれを要求しています。
面倒を見ていないので、それをカットします。
SendPreauthenticationHeadRequestIfNeeded
メソッドSendPreauthenticationHeadRequestIfNeeded
非同期であるため、パッチを適用すると、若すぎる年齢で目が赤くなることがあります。 周りを見ると、シンプルで気取らないAuthenticationSchemeMayRequireResend
メソッドに気付くでしょう。 明らかに、常にfalseを返す場合、 SendPreauthenticationHeadRequestIfNeeded
は開始されません。
操作を開始します。
ソリューションに新しいWcfPreauthPatchプロジェクトを追加します。 次に、Cecilを挿入し、ILコードに登ります。 .NET Coreで動作するにはベータ版が必要です。
Install-Package Mono.Cecil -Version 0.10.0-beta7 -ProjectName WcfPreauthPatch
コードは次のとおりです。
static void Main(string[] args) { var curlHandlerType = typeof(HttpClient).Assembly.GetTypes() .FirstOrDefault(type => type.Name == "CurlHandler"); if (curlHandlerType == null) return; // continue only when libcurl is used var wcfDllPath = typeof(System.ServiceModel.ClientBase<>) .Assembly.ManifestModule.FullyQualifiedName; var wcfAssembly = AssemblyDefinition.ReadAssembly(wcfDllPath); var requestType = wcfAssembly.MainModule.GetAllTypes() .FirstOrDefault(type => type.Name.Contains("HttpClientChannelAsyncRequest")); var authRequiredMethod = requestType.Methods .FirstOrDefault(method => method.Name.Contains("AuthenticationSchemeMayRequireResend")); authRequiredMethod.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldc_I4_0)); // put false on stack authRequiredMethod.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Ret)); wcfAssembly.Write(wcfDllPath + ".patched"); File.Delete(wcfDllPath); File.Move(wcfDllPath + ".patched", wcfDllPath); }
Dockerfileに追加します
# for build image FROM build AS build-wcf-patch WORKDIR /src/WcfPreauthPatch/ RUN dotnet build -c Debug -o /app ... # for release image FROM base AS base-wcf-preauth-fixed COPY --from=publish /app . RUN dotnet WcfPreauthPatch.dll
サービスを開始し、ログのリクエストが少なくなるようにします。
エピローグ
.NET CoreのWCFクライアントは、多くの問題をもたらしました。
githubには、記事で提起された問題と問題の議論が既にあります。
1.交渉/ NTLM
https://github.com/dotnet/corefx/issues/9533
https://github.com/dotnet/corefx/issues/9234
2.事前認証リクエスト
https://github.com/dotnet/wcf/issues/2433
しかし、見てきたように、これらの問題は完全には解決されていません。 ディスカッションの5つのコペックがプロセスに新たな方向を追加することを願っています。
軽食のためにいくつかのアイデアと事実
Dockerとの統合がないため、パッチ適用はビルド後のターゲットとして実行できます。
CNTLMなどのNTLMプロキシがあります。 コンテナ内でNTLMプロキシを構成する別の方法も可能性があり、より普遍的であり、既製のカスタマイズされたイメージはDocker Hubでの計算に値します。
仮に、WCFサービスから取得したWWW-Authentication認証ヘッダーの編集を試みることができます。 IEndpointBehaviorおよびAfterReceiveReplyメソッドを使用して、WCFクライアントの動作をオーバーライドする必要があります。 ただし、これは、事前認証要求がオフの場合にのみ機能します。 AfterReceiveReplyは彼をキャッチしません。
HttpClientを使用/使用している場合、NTLMの同様の問題の回避策へのリンクがあります。
CurlHandlerにCecilをパッチすることは機能しません:System.Net.Http.dllは混合モードアセンブリ(つまり、マネージコードとネイティブコード)であり、このオプションはCecilではまだサポートされていません。
記事で説明されている実行時のメソッドへのポインタの置き換えは機能しません。試さないでください。
- Linuxでは、.NET Coreは関数ブレークポイントをサポートしていません。