Bagaimana cara agar HttpClient lulus kredensial bersama dengan permintaan?

164

Saya memiliki aplikasi web (dihosting di IIS) yang berbicara ke layanan Windows. Layanan Windows menggunakan ASP.Net MVC Web API (self-host), dan dapat dikomunikasikan dengan lebih dari http menggunakan JSON. Aplikasi web dikonfigurasi untuk melakukan peniruan, gagasannya adalah bahwa pengguna yang membuat permintaan ke aplikasi web harus menjadi pengguna yang digunakan aplikasi web untuk membuat permintaan ke layanan. Strukturnya terlihat seperti ini:

(Pengguna yang disorot dengan warna merah adalah pengguna yang dirujuk dalam contoh di bawah ini.)


Aplikasi web membuat permintaan ke layanan Windows menggunakan HttpClient:

var httpClient = new HttpClient(new HttpClientHandler() 
                      {
                          UseDefaultCredentials = true
                      });
httpClient.GetStringAsync("http://localhost/some/endpoint/");

Ini membuat permintaan ke layanan Windows, tetapi tidak melewati kredensial dengan benar (layanan melaporkan pengguna sebagai IIS APPPOOL\ASP.NET 4.0). Ini bukan yang saya inginkan terjadi .

Jika saya mengubah kode di atas untuk menggunakan WebClient, kredensial pengguna diberikan dengan benar:

WebClient c = new WebClient
                   {
                       UseDefaultCredentials = true
                   };
c.DownloadStringAsync(new Uri("http://localhost/some/endpoint/"));

Dengan kode di atas, layanan melaporkan pengguna sebagai pengguna yang membuat permintaan ke aplikasi web.

Apa yang saya lakukan salah dengan HttpClientimplementasi yang menyebabkannya tidak lulus kredensial dengan benar (atau apakah itu bug dengan HttpClient)?

Alasan saya ingin menggunakan HttpClientadalah karena ia memiliki API async yang bekerja dengan baik dengan Tasks, sedangkan WebClientAPI asyc perlu ditangani dengan acara.

adrianbank
sumber
Kemungkinan duplikat dari stackoverflow.com/q/10308938/1045728
Tommy Grovnes
Tampaknya HttpClient dan WebClient menganggap hal-hal yang berbeda sebagai DefaultCredentials. Apakah Anda mencoba HttpClient.setCredentials (...)?
Germann Arlington
BTW, WebClient memiliki DownloadStringTaskAsync.Net 4.5, yang juga dapat digunakan dengan async / menunggu
LB
1
@GermannArlington: HttpClienttidak punya SetCredentials()metode. Bisakah Anda mengarahkan saya ke maksud Anda?
adrianbanks
4
Tampaknya ini telah diperbaiki (.net 4.5.1)? Saya mencoba membuat new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }di server web yang diakses oleh pengguna terotentikasi Windows, dan situs web melakukan otentikasi untuk sumber daya jarak jauh lain setelah itu (tidak akan mengotentikasi tanpa flag yang ditetapkan).
GSerg

Jawaban:

67

Saya juga mengalami masalah yang sama. Saya mengembangkan solusi sinkron berkat penelitian yang dilakukan oleh @tpeczek dalam artikel SO berikut: Tidak dapat mengautentikasi ke layanan ASP.NET Web Api dengan HttpClient

Solusi saya menggunakan a WebClient, yang seperti yang Anda catat dengan benar melewati kredensial tanpa masalah. Alasannya HttpClienttidak berhasil adalah karena keamanan Windows menonaktifkan kemampuan untuk membuat utas baru di bawah akun tiruan (lihat artikel SO di atas.) HttpClientMembuat utas baru melalui Pabrik Tugas sehingga menyebabkan kesalahan. WebClientdi sisi lain, berjalan secara sinkron di utas yang sama sehingga melewati aturan dan meneruskan kredensial.

Meskipun kodenya berfungsi, downside adalah bahwa itu tidak akan berfungsi async.

var wi = (System.Security.Principal.WindowsIdentity)HttpContext.Current.User.Identity;

var wic = wi.Impersonate();
try
{
    var data = JsonConvert.SerializeObject(new
    {
        Property1 = 1,
        Property2 = "blah"
    });

    using (var client = new WebClient { UseDefaultCredentials = true })
    {
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json; charset=utf-8");
        client.UploadData("http://url/api/controller", "POST", Encoding.UTF8.GetBytes(data));
    }
}
catch (Exception exc)
{
    // handle exception
}
finally
{
    wic.Undo();
}

Catatan: Memerlukan paket NuGet: Newtonsoft.Json, yang merupakan serializer JSON yang digunakan WebAPI.

Joshua
sumber
1
Saya melakukan sesuatu yang serupa pada akhirnya, dan itu bekerja dengan sangat baik. Masalah asinkron tidak menjadi masalah, karena saya ingin panggilan diblokir.
adrianbanks
136

Anda dapat mengonfigurasi HttpClientuntuk meneruskan kredensial seperti ini:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
Sean
sumber
11
Saya tahu bagaimana melakukannya. Perilaku ini bukan yang saya inginkan (sebagaimana dinyatakan dalam pertanyaan) - "Ini membuat permintaan ke layanan Windows, tetapi tidak meneruskan kredensial dengan benar (layanan melaporkan pengguna sebagai IIS APPPOOL \ ASP.NET 4.0). Ini bukan itu yang saya inginkan terjadi. "
adrianbank
4
ini tampaknya memperbaiki masalah saya di mana iis hanya mengaktifkan otentikasi windows. jika Anda hanya memerlukan beberapa kredensial yang sah untuk dilewati, ini harus dilakukan.
Timmerz
Tidak yakin ini bekerja sama dengan WebClient dalam skenario peniruan / delegasi. Saya mendapatkan "Nama pokok target salah" saat menggunakan HttpClient dengan solusi di atas, tetapi menggunakan WebClient dengan pengaturan serupa melewati kredensial pengguna.
Peder Rice
Ini berhasil bagi saya dan log menunjukkan pengguna yang benar. Meskipun, dengan hop ganda dalam gambar, saya tidak berharap untuk bekerja dengan NTLM sebagai skema otentikasi yang mendasarinya, tetapi ia bekerja.
Nitin Rastogi
Bagaimana cara melakukan hal yang sama dengan menggunakan aspnet core versi terbaru? (2.2). Jika ada yang tahu ...
Nico
26

Apa yang Anda coba lakukan adalah membuat NTLM meneruskan identitas ke server berikutnya, yang tidak dapat dilakukan - itu hanya dapat melakukan peniruan yang hanya memberi Anda akses ke sumber daya lokal. Itu tidak akan membiarkan Anda melewati batas mesin. Otentikasi Kerberos mendukung delegasi (apa yang Anda butuhkan) dengan menggunakan tiket, dan tiket dapat diteruskan ketika semua server dan aplikasi dalam rantai dikonfigurasikan dengan benar dan Kerberos diatur dengan benar di domain. Jadi, singkatnya Anda perlu beralih dari menggunakan NTLM ke Kerberos.

Untuk lebih lanjut tentang opsi Otentikasi Windows yang tersedia untuk Anda dan cara kerjanya mulai dari: http://msdn.microsoft.com/en-us/library/ff647076.aspx

BlackSpy
sumber
3
" NTLM untuk meneruskan identitas ke server berikutnya, yang tidak dapat dilakukannya " - bagaimana bisa melakukan ini saat menggunakan WebClient? Ini adalah hal yang saya tidak mengerti - jika tidak memungkinkan, kenapa hal itu adalah melakukannya?
adrianbanks
2
Saat menggunakan klien web itu masih hanya satu koneksi, antara klien dan server. Ini dapat menyamar sebagai pengguna di server itu (1 hop), tetapi tidak dapat meneruskan kredensial tersebut ke komputer lain (2 hop - klien ke server ke server ke-2). Untuk itu Anda perlu delegasi.
BlackSpy
1
Satu-satunya cara untuk mencapai apa yang Anda coba lakukan dengan cara yang Anda coba lakukan adalah membuat pengguna mengetikkan nama pengguna dan kata sandi ke dalam kotak dialog khusus pada aplikasi ASP.NET Anda, simpan sebagai string dan kemudian gunakan mereka untuk mengatur identitas Anda ketika Anda terhubung ke proyek API Web Anda. Kalau tidak, Anda harus meninggalkan NTLM dan pindah ke Kerberos, sehingga Anda bisa melewati tiket Kerboros ke proyek Web API. Saya sangat merekomendasikan membaca tautan yang saya lampirkan dalam jawaban asli saya. Apa yang Anda coba lakukan memerlukan pemahaman yang kuat tentang otentikasi windows sebelum Anda mulai.
BlackSpy
2
@ BlackSpy: Saya punya banyak pengalaman dengan Otentikasi Windows. Apa yang saya coba pahami adalah mengapa WebClientbisa meneruskan kredensial NTLM, tetapi HttpClienttidak bisa. Saya dapat mencapai ini menggunakan penyamaran ASP.Net saja, dan tidak harus menggunakan Kerberos atau untuk menyimpan nama pengguna / kata sandi. Namun ini hanya bekerja dengan WebClient.
adrianbank
1
Seharusnya tidak mungkin untuk meniru lebih dari 1 hop tanpa melewati nama pengguna dan kata sandi sebagai teks. itu melanggar aturan Peniruan, dan NTLM tidak akan mengizinkannya. WebClient memungkinkan Anda untuk melompat 1 hop karena Anda melewatkan kredensial dan berjalan sebagai pengguna di dalam kotak. Jika Anda melihat log keamanan Anda akan melihat login - pengguna login ke sistem. Anda tidak dapat berjalan sebagai pengguna dari mesin itu kecuali Anda telah melewati kredensial sebagai teks dan menggunakan contoh webclient lain untuk masuk ke kotak berikutnya.
BlackSpy
17

OK, terima kasih untuk semua kontributor di atas. Saya menggunakan .NET 4.6 dan kami juga memiliki masalah yang sama. Saya menghabiskan waktu debugging System.Net.Http, khususnya HttpClientHandler, dan menemukan yang berikut:

    if (ExecutionContext.IsFlowSuppressed())
    {
      IWebProxy webProxy = (IWebProxy) null;
      if (this.useProxy)
        webProxy = this.proxy ?? WebRequest.DefaultWebProxy;
      if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
        this.SafeCaptureIdenity(state);
    }

Jadi setelah menilai bahwa ExecutionContext.IsFlowSuppressed()mungkin itu penyebabnya, saya membungkus kode Peniruan sebagai berikut:

using (((WindowsIdentity)ExecutionContext.Current.Identity).Impersonate())
using (System.Threading.ExecutionContext.SuppressFlow())
{
    // HttpClient code goes here!
}

Kode di dalam SafeCaptureIdenity(bukan kesalahan pengejaan saya), mengambil WindowsIdentity.Current()identitas palsu kami. Ini sedang diambil karena kita sekarang menekan aliran. Karena menggunakan / membuang ini diatur ulang setelah doa.

Sekarang tampaknya bekerja untuk kita, Fiuh!

Chullybun
sumber
2
Terima kasih banyak untuk melakukan analisis ini. Ini memperbaiki situasi saya juga. Sekarang Identitas saya diteruskan dengan benar ke aplikasi web lain! Anda menyelamatkan saya jam kerja! Saya terkejut itu tidak lebih tinggi pada hitungan tick.
justdan23
Saya hanya perlu using (System.Threading.ExecutionContext.SuppressFlow())dan masalah ini diselesaikan untuk saya!
ZX9
10

NET Inti, saya berhasil mendapatkan System.Net.Http.HttpClientdengan UseDefaultCredentials = truemelewati Windows kredensial pengguna dikonfirmasi untuk layanan back end dengan menggunakan WindowsIdentity.RunImpersonated.

HttpClient client = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true } );
HttpResponseMessage response = null;

if (identity is WindowsIdentity windowsIdentity)
{
    await WindowsIdentity.RunImpersonated(windowsIdentity.AccessToken, async () =>
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url)
        response = await client.SendAsync(request);
    });
}
momok192
sumber
4

Ini bekerja untuk saya setelah saya mengatur pengguna dengan akses internet di layanan Windows.

Dalam kode saya:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 
Paulo Augusto Batista
sumber
3

Ok jadi saya mengambil kode Joshoun dan membuatnya generik. Saya tidak yakin apakah saya harus menerapkan pola singleton pada kelas SynchronousPost. Mungkin seseorang yang lebih berpengetahuan bisa membantu.

Penerapan

// Aku menganggap kamu memiliki tipe betonmu sendiri. Dalam kasus saya, saya menggunakan kode terlebih dahulu dengan kelas bernama FileCategory

FileCategory x = new FileCategory { CategoryName = "Some Bs"};
SynchronousPost<FileCategory>test= new SynchronousPost<FileCategory>();
test.PostEntity(x, "/api/ApiFileCategories"); 

Kelas Generik di sini. Anda dapat melewati semua jenis

 public class SynchronousPost<T>where T :class
    {
        public SynchronousPost()
        {
            Client = new WebClient { UseDefaultCredentials = true };
        }

        public void PostEntity(T PostThis,string ApiControllerName)//The ApiController name should be "/api/MyName/"
        {
            //this just determines the root url. 
            Client.BaseAddress = string.Format(
         (
            System.Web.HttpContext.Current.Request.Url.Port != 80) ? "{0}://{1}:{2}" : "{0}://{1}",
            System.Web.HttpContext.Current.Request.Url.Scheme,
            System.Web.HttpContext.Current.Request.Url.Host,
            System.Web.HttpContext.Current.Request.Url.Port
           );
            Client.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=utf-8");
            Client.UploadData(
                                 ApiControllerName, "Post", 
                                 Encoding.UTF8.GetBytes
                                 (
                                    JsonConvert.SerializeObject(PostThis)
                                 )
                             );  
        }
        private WebClient Client  { get; set; }
    }

Kelas Api ku terlihat seperti ini, jika kamu penasaran

public class ApiFileCategoriesController : ApiBaseController
{
    public ApiFileCategoriesController(IMshIntranetUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    public IEnumerable<FileCategory> GetFiles()
    {
        return UnitOfWork.FileCategories.GetAll().OrderBy(x=>x.CategoryName);
    }
    public FileCategory GetFile(int id)
    {
        return UnitOfWork.FileCategories.GetById(id);
    }
    //Post api/ApileFileCategories

    public HttpResponseMessage Post(FileCategory fileCategory)
    {
        UnitOfWork.FileCategories.Add(fileCategory);
        UnitOfWork.Commit(); 
        return new HttpResponseMessage();
    }
}

Saya menggunakan ninject, dan pola repo dengan unit kerja. Bagaimanapun, kelas generik di atas sangat membantu.

tersembunyi
sumber