Como fazer o HttpClient transmitir credenciais junto com a solicitação?


164

Eu tenho um aplicativo Web (hospedado no IIS) que fala com um serviço do Windows. O serviço do Windows está usando a API da Web do ASP.Net MVC (auto-hospedado) e, portanto, pode ser comunicado através de http usando JSON. O aplicativo da web está configurado para fazer representação, com a idéia de que o usuário que faz a solicitação para o aplicativo da web seja o usuário que o aplicativo da web usa para fazer a solicitação ao serviço. A estrutura fica assim:

(O usuário destacado em vermelho é o usuário referido nos exemplos abaixo.)


O aplicativo Web faz solicitações para o serviço Windows usando um HttpClient:

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

Isso faz a solicitação ao serviço do Windows, mas não passa as credenciais corretamente (o serviço relata o usuário como IIS APPPOOL\ASP.NET 4.0). Não é isso que eu quero que aconteça .

Se eu alterar o código acima para usar um WebClient, as credenciais do usuário serão passadas corretamente:

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

Com o código acima, o serviço relata o usuário como o usuário que fez a solicitação para o aplicativo Web.

O que estou fazendo de errado com a HttpClientimplementação que está fazendo com que ela não passe as credenciais corretamente (ou é um bug com o HttpClient)?

A razão pela qual desejo usar o HttpClienté que ele possui uma API assíncrona que funciona bem com Tasks, enquanto a WebClientAPI assíncrona do s precisa ser manipulada com eventos.



Parece que HttpClient e WebClient consideram coisas diferentes como DefaultCredentials. Você tentou HttpClient.setCredentials (...)?
Germann Arlington

BTW, WebClient tem DownloadStringTaskAsyncem .Net 4.5, que também pode ser usado com async / await
LB

1
@ GermannArlington: HttpClientnão possui um SetCredentials()método. Você pode me indicar o que você quer dizer?
precisa saber é o seguinte

4
Parece que isso foi corrigido (.net 4.5.1)? Tentei criar new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }em um servidor da web acessado por um usuário autenticado pelo Windows, e o site foi autenticado para outro recurso remoto depois disso (não seria autenticado sem o sinalizador definido).
precisa saber é o seguinte

Respostas:


67

Eu também estava tendo o mesmo problema. Desenvolvi uma solução síncrona graças à pesquisa realizada por @tpeczek no seguinte artigo da SO: Não foi possível autenticar no serviço ASP.NET Web Api com HttpClient

Minha solução usa a WebClient, que, como você observou corretamente, passa as credenciais sem problemas. O motivo HttpClientnão funciona é porque a segurança do Windows desabilita a capacidade de criar novos threads em uma conta representada (consulte o artigo SO acima.) HttpClientCria novos threads por meio do Task Factory, causando o erro. WebClientpor outro lado, é executado de forma síncrona no mesmo encadeamento, ignorando a regra e encaminhando suas credenciais.

Embora o código funcione, a desvantagem é que ele não funcionará de forma assíncrona.

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

Nota: Requer o pacote NuGet: Newtonsoft.Json, que é o mesmo serializador JSON que a WebAPI usa.


1
Fiz algo semelhante no final, e funciona muito bem. O problema assíncrono não é um problema, pois quero que as chamadas sejam bloqueadas.
precisa saber é o seguinte

136

Você pode configurar HttpClientpara passar credenciais automaticamente assim:

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

11
Eu sei como fazer isso. O comportamento não é o que eu quero (como indicado na pergunta) - "Isso faz a solicitação ao serviço do Windows, mas não passa as credenciais corretamente (o serviço relata o usuário como IIS APPPOOL \ ASP.NET 4.0). Isso não é o que eu quero que aconteça. "
precisa saber é o seguinte

4
isso parece corrigir o problema em que o iis só tem a autenticação do windows ativada. se você precisar apenas de algumas credenciais legítimas, isso deve ser feito.
Timmerz 22/01

Não tenho certeza se isso funciona da mesma forma que o WebClient em cenários de representação / delegação. Recebo "O nome do principal de destino está incorreto" ao usar o HttpClient com a solução acima, mas usar o WebClient com uma configuração semelhante passa as credenciais do usuário.
Peder Rice

Isso funcionou para mim e os logs mostram o usuário correto. Embora, com o salto duplo na imagem, eu não esperava que funcionasse com o NTLM como o esquema de autenticação subjacente, mas funciona.
Nitin Rastogi

Como fazer o mesmo usando a versão mais recente do aspnet core? (2.2) Se alguém souber ...
Nico

26

O que você está tentando fazer é fazer com que o NTLM encaminhe a identidade para o próximo servidor, o que não pode ser feito - ele só pode fazer a representação, o que apenas lhe dá acesso aos recursos locais. Não permitirá que você cruze os limites de uma máquina. A autenticação Kerberos oferece suporte à delegação (o que você precisa) usando tickets, e o ticket pode ser encaminhado quando todos os servidores e aplicativos da cadeia estiverem configurados corretamente e o Kerberos estiver configurado corretamente no domínio. Portanto, em resumo, você precisa mudar do NTLM para o Kerberos.

Para saber mais sobre as opções de autenticação do Windows disponíveis para você e como elas funcionam, comece em: http://msdn.microsoft.com/en-us/library/ff647076.aspx


3
" NTLM para encaminhar a identidade para o próximo servidor, o que não pode ser feito " - como é que isso acontece quando se usa WebClient? É isso que eu não entendo - se não é possível, por que está fazendo isso?
adrianbanks

2
Ao usar o Web client, ainda há apenas uma conexão entre o cliente e o servidor. Ele pode representar o usuário nesse servidor (1 salto), mas não pode encaminhar essas credenciais para outra máquina (2 saltos - cliente para servidor para o segundo servidor). Para isso você precisa de delegação.
BlackSpy

1
A única maneira de realizar o que você está tentando fazer da maneira que está tentando fazer é fazer com que o usuário digite seu nome de usuário e senha em uma caixa de diálogo personalizada em seu aplicativo ASP.NET, armazene-os como seqüências de caracteres e use para definir sua identidade quando você se conectar ao seu projeto de API da Web. Caso contrário, você precisará descartar o NTLM e passar para o Kerberos, para que você possa passar o tíquete do Kerboros para o projeto de API da Web. Eu recomendo a leitura do link que anexei na minha resposta original. O que você está tentando fazer requer um forte entendimento da autenticação do Windows antes de começar.
BlackSpy

2
@BlackSpy: Tenho muita experiência com a autenticação do Windows. O que estou tentando entender é por que o WebClientpode transmitir as credenciais NTLM, mas o HttpClientnão pode. Eu posso conseguir isso usando a representação do ASP.Net sozinha e sem ter que usar o Kerberos ou armazenar nomes de usuário / senhas. No entanto, isso funciona com WebClient.
adrianbanks

1
Deve ser impossível se passar por mais de 1 salto sem passar o nome de usuário e a senha como texto. quebra as regras de representação, e o NTLM não permitirá. O WebClient permite que você pule 1 salto porque você passa as credenciais e executa como esse usuário na caixa. Se você olhar para os logs de segurança, verá o logon - o usuário efetua login no sistema. Você não pode executar como usuário a partir dessa máquina, a menos que tenha passado as credenciais como texto e use outra instância do cliente da Web para fazer logon na próxima caixa.
BlackSpy

17

OK, obrigado por todos os colaboradores acima. Estou usando o .NET 4.6 e também tivemos o mesmo problema. Passei um tempo depurando System.Net.Http, especificamente o HttpClientHandler, e encontrei o seguinte:

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

Então, depois de avaliar que o ExecutionContext.IsFlowSuppressed()culpado pode ter sido o culpado, envolvi nosso código de representação da seguinte maneira:

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

O código dentro de SafeCaptureIdenity(não meu erro de ortografia), pega WindowsIdentity.Current()qual é a nossa identidade representada. Isso está sendo percebido porque agora estamos suprimindo o fluxo. Devido ao uso / descarte, isso é redefinido após a chamada.

Agora parece funcionar para nós, ufa!


2
Muito obrigado por fazer essa análise. Isso corrigiu minha situação também. Agora minha identidade é passada corretamente para o outro aplicativo da web! Você me salvou horas de trabalho! Estou surpreso que não seja maior na contagem de carrapatos.
justdan23

Eu só precisava using (System.Threading.ExecutionContext.SuppressFlow())e o problema foi resolvido para mim!
ZX9 13/04

10

Em .NET Core, eu consegui obter uma System.Net.Http.HttpClientcom UseDefaultCredentials = truea passar por credenciais do Windows do usuário autenticado para um serviço de back-end usando 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

Funcionou para mim depois de configurar um usuário com acesso à Internet no serviço Windows.

No meu código:

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

3

Ok, então peguei o código Joshoun e o tornei genérico. Não tenho certeza se devo implementar o padrão singleton na classe SynchronousPost. Talvez alguém com mais conhecimento possa ajudar.

Implementação

// Presumo que você tenha seu próprio tipo concreto. No meu caso, eu estou usando o código primeiro com uma classe chamada FileCategory

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

Classe genérica aqui. Você pode passar qualquer tipo

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

Minhas aulas de API são assim, se você estiver curioso

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

Estou usando o ninject e o padrão de repo com a unidade de trabalho. De qualquer forma, a classe genérica acima realmente ajuda.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.