จะทำให้ HttpClient ส่งผ่านข้อมูลประจำตัวพร้อมกับคำขอได้อย่างไร


164

ฉันมีเว็บแอปพลิเคชั่น (โฮสต์ใน IIS) ที่พูดคุยกับบริการของ Windows บริการ Windows กำลังใช้ ASP.Net MVC Web API (โฮสต์ด้วยตนเอง) และสามารถสื่อสารกับผ่าน http โดยใช้ JSON แอปพลิเคชันเว็บได้รับการกำหนดค่าให้ทำการแอบอ้างเป็นแนวคิดที่ว่าผู้ใช้ที่ทำการร้องขอไปยังเว็บแอปพลิเคชันควรเป็นผู้ใช้ที่เว็บแอปพลิเคชันใช้ในการร้องขอบริการ โครงสร้างมีลักษณะดังนี้:

(ผู้ใช้ที่เน้นด้วยสีแดงคือผู้ใช้ที่ถูกอ้างถึงในตัวอย่างด้านล่าง)


เว็บแอปพลิเคชันส่งคำขอไปยังบริการ Windows โดยใช้HttpClient:

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

สิ่งนี้ทำให้การร้องขอไปยังบริการ Windows แต่ไม่ผ่านข้อมูลประจำตัวอย่างถูกต้อง (บริการรายงานผู้ใช้เป็นIIS APPPOOL\ASP.NET 4.0) นี้ไม่ได้เป็นสิ่งที่ฉันต้องการให้เกิดขึ้น

ถ้าฉันเปลี่ยนรหัสข้างต้นเพื่อใช้WebClientแทนข้อมูลประจำตัวของผู้ใช้จะถูกส่งผ่านอย่างถูกต้อง:

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

ด้วยรหัสด้านบนบริการรายงานผู้ใช้เป็นผู้ใช้ที่ร้องขอไปยังเว็บแอปพลิเคชัน

ฉันทำอะไรผิดกับการHttpClientใช้งานที่ทำให้ไม่ผ่านข้อมูลประจำตัวอย่างถูกต้อง (หรือเป็นจุดบกพร่องด้วยHttpClient)

เหตุผลที่ฉันต้องการใช้HttpClientคือมี async API ที่ทำงานได้ดีกับTasks ในขณะที่WebClientasyc API ของต้องจัดการกับเหตุการณ์


สำเนาซ้ำที่เป็นไปได้ของstackoverflow.com/q/10308938/1045728
Tommy Grovnes

ดูเหมือนว่า HttpClient และ WebClient จะพิจารณาสิ่งต่าง ๆ ให้เป็น DefaultCredentials คุณลอง HttpClient.setCredentials (... ) หรือยัง
Germann Arlington

BTW, WebClient มีDownloadStringTaskAsyncใน. Net 4.5 ซึ่งสามารถใช้กับ async / await ได้
LB

1
@GermannArlington: HttpClientไม่มีSetCredentials()วิธี คุณช่วยชี้ให้ฉันในสิ่งที่คุณหมายถึง?
adrianbanks

4
มันจะปรากฏขึ้นนี้ได้รับการแก้ไข (.net 4.5.1)? ฉันพยายามสร้างnew HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }บนเว็บเซิร์ฟเวอร์ที่เข้าถึงโดยผู้ใช้ที่ได้รับการรับรองความถูกต้องของ Windows และเว็บไซต์ทำการตรวจสอบสิทธิ์สำหรับทรัพยากรระยะไกลอื่นหลังจากนั้น (จะไม่รับรองความถูกต้องหากไม่มีชุดธง)
GSerg

คำตอบ:


67

ฉันก็มีปัญหาเดียวกันนี้เช่นกัน ฉันพัฒนาโซลูชันแบบซิงโครนัสด้วยการวิจัยที่ทำโดย @tpeczek ในบทความ SO ต่อไปนี้: ไม่สามารถตรวจสอบสิทธิ์กับบริการ ASP.NET Web Api ด้วย HttpClient

โซลูชันของฉันใช้ a WebClientซึ่งตามที่คุณจดไว้อย่างถูกต้องผ่านการรับรองโดยไม่มีปัญหา เหตุผลที่HttpClientไม่ทำงานเป็นเพราะความปลอดภัยของ Windows ปิดการใช้งานความสามารถในการสร้างหัวข้อใหม่ภายใต้บัญชีปลอม (ดูบทความดังกล่าวข้างต้น) HttpClientสร้างหัวข้อใหม่ผ่านโรงงานงานจึงก่อให้เกิดข้อผิดพลาด WebClientในอีกทางหนึ่งวิ่งพร้อมกันในหัวข้อเดียวกันจึงข้ามกฎและส่งต่อข้อมูลประจำตัว

ถึงแม้ว่ารหัสจะทำงานได้ แต่ข้อเสียคือมันจะไม่ทำงานแบบ 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();
}

หมายเหตุ:ต้องใช้แพคเกจ NuGet: Newtonsoft.Json ซึ่งใช้ JSAP serializer WebAPI เดียวกัน


1
ในที่สุดฉันก็ทำสิ่งที่คล้ายกันและใช้งานได้ดีจริงๆ ปัญหาแบบอะซิงโครนัสไม่ใช่ปัญหาตามที่ฉันต้องการให้สายการบล็อก
adrianbanks

136

คุณสามารถกำหนดค่าHttpClientให้ส่งข้อมูลรับรองโดยอัตโนมัติดังนี้:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });

11
ฉันรู้วิธีการทำ พฤติกรรมไม่ใช่สิ่งที่ฉันต้องการ (ตามที่ระบุในคำถาม) - "สิ่งนี้ทำให้การร้องขอไปยังบริการ Windows แต่ไม่ผ่านข้อมูลประจำตัวอย่างถูกต้อง (บริการรายงานผู้ใช้เป็น IIS APPPOOL \ ASP.NET 4.0) ไม่ใช่สิ่งที่ฉันต้องการจะเกิดขึ้น "
adrianbanks

4
ดูเหมือนว่าจะแก้ไขปัญหาของฉันเมื่อ iis เปิดใช้งานการตรวจสอบสิทธิ์ Windows เท่านั้น หากคุณต้องการข้อมูลรับรองที่ถูกต้องบางอย่างผ่านไปสิ่งนี้ควรทำ
Timmerz

ไม่แน่ใจว่าสิ่งนี้ทำงานเหมือนกับ WebClient ในสถานการณ์การรับบทบาท / การมอบหมาย ฉันได้รับ "ชื่อหลักเป้าหมายไม่ถูกต้อง" เมื่อใช้ HttpClient ด้วยวิธีแก้ไขปัญหาข้างต้น แต่การใช้ WebClient ด้วยการตั้งค่าที่คล้ายกันจะส่งผ่านข้อมูลรับรองของผู้ใช้ผ่าน
Peder Rice

นี่ใช้งานได้สำหรับฉันและบันทึกแสดงผู้ใช้ที่ถูกต้อง แม้ว่าด้วยการกระโดดสองครั้งในภาพฉันไม่ได้คาดหวังให้ทำงานกับ NTLM เป็นรูปแบบการตรวจสอบสิทธิ์พื้นฐาน แต่ก็ใช้งานได้
Nitin Rastogi

จะทำเช่นเดียวกันโดยใช้ aspnet รุ่นล่าสุดได้อย่างไร (2.2) ถ้าใครรู้ ...
Nico

26

สิ่งที่คุณพยายามทำคือให้ NTLM ส่งต่อข้อมูลประจำตัวไปยังเซิร์ฟเวอร์ถัดไปซึ่งไม่สามารถทำได้ - สามารถทำเลียนแบบได้ซึ่งให้สิทธิ์การเข้าถึงทรัพยากรท้องถิ่นเท่านั้น มันจะไม่ยอมให้คุณข้ามขอบเขตของเครื่องจักร การรับรองความถูกต้อง Kerberos สนับสนุนการมอบหมาย (สิ่งที่คุณต้องการ) โดยใช้บัตรและสามารถส่งต่อตั๋วได้เมื่อเซิร์ฟเวอร์และแอปพลิเคชันทั้งหมดในเครือข่ายได้รับการกำหนดค่าอย่างถูกต้องและ Kerberos ตั้งค่าอย่างถูกต้องบนโดเมน ดังนั้นในระยะสั้นคุณต้องเปลี่ยนจากการใช้ NTLM เป็น Kerberos

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับตัวเลือกการรับรองความถูกต้องของ Windows ที่มีให้คุณและวิธีการทำงานเริ่มต้นที่: http://msdn.microsoft.com/en-us/library/ff647076.aspx


3
" NTLM เพื่อส่งต่อข้อมูลประจำตัวไปยังเซิร์ฟเวอร์ถัดไปซึ่งไม่สามารถทำได้ " - จะทำเช่นไรเมื่อใช้WebClient? นี่คือสิ่งที่ฉันไม่เข้าใจ - ถ้ามันเป็นไปไม่ได้ว่ามามันจะทำหรือไม่
adrianbanks

2
เมื่อใช้เว็บไคลเอ็นต์จะยังคงมีเพียงการเชื่อมต่อเดียวระหว่างไคลเอนต์และเซิร์ฟเวอร์ มันสามารถเลียนแบบผู้ใช้บนเซิร์ฟเวอร์นั้น (1 ฮอป) แต่ไม่สามารถส่งต่อข้อมูลรับรองเหล่านั้นไปยังเครื่องอื่น (2 ฮ็อป - ไคลเอนต์ไปยังเซิร์ฟเวอร์ไปยังเซิร์ฟเวอร์ที่สอง) เพื่อที่คุณจะต้องมีการมอบหมาย
BlackSpy

1
วิธีเดียวที่จะบรรลุสิ่งที่คุณพยายามทำในลักษณะที่คุณพยายามทำคือให้ผู้ใช้พิมพ์ชื่อผู้ใช้และรหัสผ่านลงในกล่องโต้ตอบแบบกำหนดเองในแอปพลิเคชัน ASP.NET ของคุณเก็บไว้เป็นสตริงแล้วใช้ พวกเขาเพื่อกำหนดตัวตนของคุณเมื่อคุณเชื่อมต่อกับโครงการ Web API ของคุณ มิฉะนั้นคุณต้องวาง NTLM และย้ายไปที่ Kerberos เพื่อให้คุณสามารถส่งตั๋ว Kerboros ไปยังโครงการ Web API ฉันขอแนะนำให้อ่านลิงก์ที่แนบมากับคำตอบดั้งเดิมของฉัน สิ่งที่คุณพยายามทำนั้นจำเป็นต้องมีความเข้าใจที่ดีเกี่ยวกับการรับรองความถูกต้องของ windows ก่อนที่จะเริ่ม
BlackSpy

2
@BlackSpy: ฉันมีประสบการณ์มากมายกับ Windows Authentication สิ่งที่ฉันพยายามเข้าใจคือสาเหตุที่WebClientสามารถส่งผ่านข้อมูลประจำตัว NTLM แต่HttpClientไม่สามารถทำได้ ฉันสามารถทำสิ่งนี้ได้โดยใช้การแอบอ้าง ASP.Net เพียงอย่างเดียวและไม่ต้องใช้ Kerberos หรือเพื่อเก็บชื่อผู้ใช้ / รหัสผ่าน อย่างไรก็ตามเรื่องนี้เท่านั้นWebClientทำงานร่วมกับ
adrianbanks

1
มันเป็นไปไม่ได้ที่จะแอบอ้างเป็นตัวตนมากกว่า 1 hop โดยไม่ต้องใส่ชื่อผู้ใช้และรหัสผ่านเป็นข้อความ มันแบ่งกฎของการเลียนแบบและ NTLM จะไม่อนุญาต WebClient อนุญาตให้คุณกระโดดได้ 1 hop เนื่องจากคุณผ่านการรับรองและเรียกใช้ในฐานะผู้ใช้นั้นในกล่อง หากคุณดูบันทึกความปลอดภัยคุณจะเห็นการเข้าสู่ระบบ - ผู้ใช้เข้าสู่ระบบ จากนั้นคุณจะไม่สามารถเรียกใช้ในฐานะผู้ใช้รายนั้นจากเครื่องนั้นเว้นแต่ว่าคุณได้ส่งข้อมูลรับรองเป็นข้อความและใช้อินสแตนซ์ของเว็บอื่นเพื่อเข้าสู่กล่องถัดไป
BlackSpy

17

ตกลงดังนั้นขอขอบคุณผู้มีส่วนร่วมทั้งหมดข้างต้น ฉันใช้. NET 4.6 และเราก็มีปัญหาเดียวกัน ฉันใช้การดีบักเวลาSystem.Net.HttpโดยเฉพาะHttpClientHandlerและพบสิ่งต่อไปนี้:

    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);
    }

ดังนั้นหลังจากประเมินว่าสิ่งที่ExecutionContext.IsFlowSuppressed()อาจเป็นตัวการฉันได้ใส่รหัสการเลียนแบบของเราดังนี้:

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

โค้ดด้านในของSafeCaptureIdenity(ไม่ใช่การสะกดคำผิดของฉัน), คว้าWindowsIdentity.Current()ซึ่งเป็นตัวตนของเราเลียนแบบ สิ่งนี้กำลังถูกหยิบขึ้นมาเพราะตอนนี้เราหยุดการไหลของ เนื่องจากการใช้ / การกำจัดนี้ถูกรีเซ็ตหลังจากการเรียกใช้

ตอนนี้ดูเหมือนว่าจะทำงานให้เราว้า!


2
ขอบคุณมากที่ทำการวิเคราะห์นี้ นี่เป็นการแก้ไขสถานการณ์ของฉันด้วย ตอนนี้ตัวตนของฉันถูกส่งผ่านอย่างถูกต้องไปยังโปรแกรมประยุกต์บนเว็บอื่น ๆ ! คุณช่วยฉันชั่วโมงทำงาน! ฉันประหลาดใจที่ไม่นับจำนวนติ๊กที่สูงขึ้น
justdan23

ฉันแค่ต้องการusing (System.Threading.ExecutionContext.SuppressFlow())และปัญหาก็แก้ไขได้สำหรับฉัน!
ZX9

10

ใน .NET แกนฉันจัดการเพื่อให้ได้System.Net.Http.HttpClientด้วยUseDefaultCredentials = trueผ่านข้อมูลประจำตัวของ Windows 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);
    });
}

4

มันทำงานสำหรับฉันหลังจากที่ฉันตั้งค่าผู้ใช้ที่มีการเข้าถึงอินเทอร์เน็ตในบริการ Windows

ในรหัสของฉัน:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 

3

ตกลงดังนั้นฉันจึงใช้รหัส Joshoun และทำให้เป็นรหัสทั่วไป ฉันไม่แน่ใจว่าฉันควรใช้รูปแบบซิงเกิลในคลาส SynchronousPost หรือไม่ บางทีคนที่มีความรู้มากกว่านี้สามารถช่วยได้

การดำเนินงาน

// ฉันคิดว่าคุณมีรูปแบบที่เป็นรูปธรรมของคุณเอง ในกรณีของฉันฉันใช้รหัสก่อนกับคลาสที่เรียกว่า FileCategory

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

ชั้นเรียนทั่วไปที่นี่ คุณสามารถผ่านประเภทใดก็ได้

 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; }
    }

คลาส Api ของฉันเป็นแบบนี้ถ้าคุณอยากรู้

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();
    }
}

ฉันใช้ ninject และรูปแบบ repo กับหน่วยงาน อย่างไรก็ตามคลาสทั่วไปข้างต้นช่วยได้จริงๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.