Response.Redirect com POST em vez de Get?

248

Temos o requisito de aceitar o envio de um formulário e salvar alguns dados e, em seguida, redirecionar o usuário para uma página externa, mas no redirecionamento, precisamos "enviar" um formulário com POST, não GET.

Eu esperava que houvesse uma maneira fácil de conseguir isso, mas estou começando a pensar que não há. Acho que agora devo criar uma outra página simples, com apenas o formulário que eu quero, redirecionar para ela, preencher as variáveis ​​do formulário e, em seguida, fazer uma chamada body.onload para um script que apenas chama document.forms [0] .submit ( );

Alguém pode me dizer se existe uma alternativa? Podemos precisar ajustar isso mais tarde no projeto, e isso pode ficar um pouco complicado, por isso, se houvesse uma maneira fácil, poderíamos fazer isso dependendo de outras páginas que não fossem outras, o que seria fantástico.

De qualquer forma, obrigado por todas e quaisquer respostas.

Matt Dawdy
fonte
No PHP, você pode enviar dados POST com cURL. Existe algo comparável para o .NET?
Brian Warshaw
Eu acho que essa é a resposta fácil que você estava procurando. Eu não podia acreditar o quão engenhoso é ... stackoverflow.com/a/6062248/110549
JoeCool
@BrianWarshaw Acho o System.Net.Http.HttpClient msdn.microsoft.com/en-us/library/… muito intuitivo e rápido de usar.
Stoyan Dimov

Respostas:

228

Para isso, é necessário entender como os redirecionamentos HTTP funcionam. Ao usar Response.Redirect(), você envia uma resposta (para o navegador que fez a solicitação) com o Código de Status HTTP 302 , que informa ao navegador para onde ir. Por definição, o navegador fará isso por meio de uma GETsolicitação, mesmo que a solicitação original fosse a POST.

Outra opção é usar o Código de Status HTTP 307 , que especifica que o navegador deve fazer a solicitação de redirecionamento da mesma maneira que a solicitação original, mas solicitar ao usuário um aviso de segurança. Para fazer isso, você escreveria algo como isto:

public void PageLoad(object sender, EventArgs e)
{
    // Process the post on your side   

    Response.Status = "307 Temporary Redirect";
    Response.AddHeader("Location", "http://example.com/page/to/post.to");
}

Infelizmente, isso nem sempre funciona. Navegadores diferentes implementam isso de maneira diferente , pois não é um código de status comum.

Infelizmente, ao contrário dos desenvolvedores Opera e FireFox, os desenvolvedores do IE nunca leram as especificações e até o IE7 mais recente e seguro redirecionará a solicitação POST do domínio A para o domínio B sem nenhum aviso ou caixa de diálogo de confirmação! O Safari também age de uma maneira interessante, embora não exiba uma caixa de diálogo de confirmação e execute o redirecionamento, ele joga fora os dados do POST, alterando efetivamente o redirecionamento 307 para o 302 mais comum.

Então, até onde eu sei, a única maneira de implementar algo assim seria usar Javascript. Existem duas opções em que consigo pensar:

  1. Crie o formulário e faça com que seu actionatributo aponte para o servidor de terceiros. Em seguida, adicione um evento de clique ao botão enviar que primeiro executa uma solicitação AJAX no servidor com os dados e, em seguida, permite que o formulário seja enviado ao servidor de terceiros.
  2. Crie o formulário para postar no seu servidor. Quando o formulário for enviado, mostre ao usuário uma página que possui um formulário com todos os dados que você deseja transmitir, todos em entradas ocultas. Apenas mostre uma mensagem como "Redirecionando ...". Em seguida, adicione um evento javascript à página que envia o formulário ao servidor de terceiros.

Dos dois, eu escolheria o segundo, por duas razões. Primeiro, é mais confiável que o primeiro, porque o Javascript não é necessário para que ele funcione; para aqueles que não o têm ativado, você sempre pode tornar visível o botão de envio do formulário oculto e instruí-lo a pressioná-lo se levar mais de 5 segundos. Segundo, você pode decidir quais dados serão transmitidos ao servidor de terceiros; se você usar apenas o processamento do formulário, ele passará todos os dados da postagem, o que nem sempre é o que você deseja. O mesmo para a solução 307, supondo que funcionasse para todos os seus usuários.

Espero que isto ajude!

tghw
fonte
1
Altere "adicionar um evento de clique ao botão enviar" para "adicionar um evento de envio ao formulário". Há mais de um método para enviar o formulário, por exemplo, pressionando Enter com qualquer entrada de texto focada, sem falar no envio programático.
temoto
122

Você pode usar esta abordagem:

Response.Clear();

StringBuilder sb = new StringBuilder();
sb.Append("<html>");
sb.AppendFormat(@"<body onload='document.forms[""form""].submit()'>");
sb.AppendFormat("<form name='form' action='{0}' method='post'>",postbackUrl);
sb.AppendFormat("<input type='hidden' name='id' value='{0}'>", id);
// Other params go here
sb.Append("</form>");
sb.Append("</body>");
sb.Append("</html>");

Response.Write(sb.ToString());

Response.End();

Como resultado, logo após o cliente obter todo o html do servidor, ocorre a sobrecarga de eventos que aciona o envio do formulário e publica todos os dados no postbackUrl definido.

Pavlo Neiman
fonte
9
+1 (eu adicionaria mais você, se pudesse). Esta é a resposta. Eloquente e direto ao ponto. Você até incluiu todo o código para fazê-lo, em vez de falar mal da sua cabeça. Eu usei isso em um iframe na minha página aspx e renderizou tudo perfeitamente - sem reescrever URLs. Excelente trabalho! DICA para aqueles que usam iframe: aponto meu iframe para outra página aspx que executa esse código.
MikeTeeVee
1
Isso funciona! A única coisa ruim que vejo é que, se você pressionar o botão Voltar do navegador, ele fará a solicitação novamente, para que você não possa mais acessar a página anterior. Existe uma solução alternativa para isso?
Mt. Schneiders
4
Há uma razão para o método suportado para fazer isso estar usando um 307. As ações POST devem ser transações idempotentes. Essa resposta é apenas um hack que funciona e pode ser facilmente bloqueado por navegadores no futuro.
Sam Rueby
2
@ Sam bom ponto. Como contra-argumento: de acordo com a resposta principal, o desenvolvedor do IE nem leu cerca de 307. Outros que o leram o implementaram de maneira errada. Isso significa que o 307 claramente é confuso para os inteligentes (assumindo que os desenvolvedores de navegadores sejam inteligentes) e propenso a erros de interpretação. A abordagem acima é clara, pelo menos para mim, e funciona em todos os navegadores do passado e do presente. Não estamos muito preocupados com o futuro, já que desenvolvedores lutamos contra o passado (leia o IE7) e os dias atuais. IMHO, já que todo mundo entendeu direito, deve mantê-lo como está. Qual seria a justificativa para bloqueá-lo no futuro?
so_mv
2
Como isso não é efetivamente o mesmo que a segunda opção do TGHW? Você também não está potencialmente pedindo a alguém descuidado para introduzir uma vulnerabilidade XSS?
Scott Scott
33

HttpWebRequest é usado para isso.

Na postagem, crie um HttpWebRequest para terceiros e publique os dados do formulário; assim que isso for feito, você poderá Response.Redirect para onde quiser.

Você tem a vantagem adicional de não precisar nomear todos os controles do servidor para formar o formulário de terceiros. Você pode fazer essa tradução ao criar a string POST.

string url = "3rd Party Url";

StringBuilder postData = new StringBuilder();

postData.Append("first_name=" + HttpUtility.UrlEncode(txtFirstName.Text) + "&");
postData.Append("last_name=" + HttpUtility.UrlEncode(txtLastName.Text));

//ETC for all Form Elements

// Now to Send Data.
StreamWriter writer = null;

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";                        
request.ContentLength = postData.ToString().Length;
try
{
    writer = new StreamWriter(request.GetRequestStream());
    writer.Write(postData.ToString());
}
finally
{
    if (writer != null)
        writer.Close();
}

Response.Redirect("NewPage");

No entanto, se você precisar que o usuário veja a página de resposta neste formulário, sua única opção é utilizar o Server.Transfer e isso pode ou não funcionar.

FlySwat
fonte
é o que eu vou usar se eu quiser um formulário extra do que o formulário asp.net. Preciso postar alguns dados em um URL externo que seja para o pagamento seguro em 3D, então preciso obter as informações retornadas da solicitação. É assim que se faz? Obrigado
Barbaros Alp
Se você precisar que o usuário veja a resposta, basta enviar de volta o conteúdo e os cabeçalhos apropriados. Pode ser necessário modificar alguns aspectos do resultado, dependendo do uso de recursos relativos, mas é certamente possível.
Thomas S. Trias
6
O usuário pode ter dados de cookies que não seriam transmitidos por esse método, pois isso está acontecendo no servidor.
Scott Scott
7

Isso deve tornar a vida muito mais fácil. Você pode simplesmente usar o método Response.RedirectWithData (...) em seu aplicativo Web facilmente.

Imports System.Web
Imports System.Runtime.CompilerServices

Module WebExtensions

    <Extension()> _
    Public Sub RedirectWithData(ByRef aThis As HttpResponse, ByVal aDestination As String, _
                                ByVal aData As NameValueCollection)
        aThis.Clear()
        Dim sb As StringBuilder = New StringBuilder()

        sb.Append("<html>")
        sb.AppendFormat("<body onload='document.forms[""form""].submit()'>")
        sb.AppendFormat("<form name='form' action='{0}' method='post'>", aDestination)

        For Each key As String In aData
            sb.AppendFormat("<input type='hidden' name='{0}' value='{1}' />", key, aData(key))
        Next

        sb.Append("</form>")
        sb.Append("</body>")
        sb.Append("</html>")

        aThis.Write(sb.ToString())

        aThis.End()
    End Sub

End Module
ZooZ
fonte
6

Algo novo no ASP.Net 3.5 é essa propriedade "PostBackUrl" dos botões ASP. Você pode configurá-lo para o endereço da página na qual deseja postar diretamente e, quando esse botão é clicado, em vez de postar de volta para a mesma página normalmente, ele é postado na página que você indicou. Handy. Certifique-se de que UseSubmitBehavior também esteja definido como TRUE.

Mike K
fonte
4

Achei interessante compartilhar que o heroku faz isso com seu SSO para provedores de complementos

Um exemplo de como funciona pode ser visto na fonte da ferramenta "kensa":

https://github.com/heroku/kensa/blob/d4a56d50dcbebc2d26a4950081acda988937ee10/lib/heroku/kensa/post_proxy.rb

E pode ser visto na prática se você ativar o javascript. Origem da página de exemplo:

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Heroku Add-ons SSO</title>
  </head>

  <body>
    <form method="POST" action="https://XXXXXXXX/sso/login">

        <input type="hidden" name="email" value="XXXXXXXX" />

        <input type="hidden" name="app" value="XXXXXXXXXX" />

        <input type="hidden" name="id" value="XXXXXXXX" />

        <input type="hidden" name="timestamp" value="1382728968" />

        <input type="hidden" name="token" value="XXXXXXX" />

        <input type="hidden" name="nav-data" value="XXXXXXXXX" />

    </form>

    <script type="text/javascript">
      document.forms[0].submit();
    </script>
  </body>
</html>
jacob
fonte
3

PostbackUrl pode ser definido no seu botão asp para postar em uma página diferente.

se você precisar fazer isso no code-behind, tente Server.Transfer.

Jimmy
fonte
2

@Matt,

Você ainda pode usar o HttpWebRequest e, em seguida, direcionar a resposta que você recebe para a resposta real do fluxo de saída, isso serviria a resposta de volta ao usuário. O único problema é que quaisquer URLs relativos seriam quebrados.

Ainda assim, isso pode funcionar.

FlySwat
fonte
2

Aqui está o que eu faria:

Coloque os dados em um formulário padrão (sem o atributo runat = "server") e defina a ação do formulário para postar na página externa de destino. Antes de enviar, eu enviava os dados ao meu servidor usando um XmlHttpRequest e analisava a resposta. Se a resposta significa que você deve prosseguir com o POST externo, então eu (o JavaScript) continuaria com a postagem, caso contrário eu redirecionaria para uma página do meu site

Andrei Rînea
fonte
Isso funciona, mas é importante observar que você perde todas as funcionalidades do servidor ASP.NET.
senfo 28/12/08
2

No PHP, você pode enviar dados POST com cURL. Existe algo comparável para o .NET?

Sim, HttpWebRequest, veja meu post abaixo.

FlySwat
fonte
2

O método GET (e HEAD) nunca deve ser usado para fazer algo que tenha efeitos colaterais. Um efeito colateral pode estar atualizando o estado de um aplicativo Web ou pode estar cobrando no seu cartão de crédito. Se uma ação tiver efeitos colaterais, outro método (POST) deve ser usado.

Portanto, um usuário (ou seu navegador) não deve ser responsabilizado por algo feito por um GET. Se algum efeito colateral prejudicial ou caro ocorrer como resultado de um GET, isso seria culpa do aplicativo da Web, não do usuário. De acordo com as especificações, um agente de usuário não deve seguir automaticamente um redirecionamento, a menos que seja uma resposta a uma solicitação GET ou HEAD.

Obviamente, muitas solicitações GET têm alguns efeitos colaterais, mesmo que sejam apenas anexadas a um arquivo de log. O importante é que o aplicativo, não o usuário, seja responsabilizado por esses efeitos.

As seções relevantes da especificação HTTP são 9.1.1 e 9.1.2 e 10.3 .

erickson
fonte
1

Sugiro a criação de um HttpWebRequest para executar programaticamente seu POST e, em seguida, redirecionar depois de ler a resposta, se aplicável.

Ben Griswold
fonte
1

Código passível de cópia com base no método de Pavlo Neyman

RedirectPost (string url, T bodyPayload) e GetPostData () são para aqueles que desejam apenas despejar alguns dados fortemente digitados na página de origem e recuperá-los no destino. Os dados devem ser serializáveis ​​pelo NewtonSoft Json.NET e você precisa fazer referência à biblioteca, é claro.

Apenas copie e cole nas suas páginas ou, melhor ainda, na classe base das suas páginas e use-a em qualquer lugar do seu aplicativo.

Meu coração está com todos vocês que ainda precisam usar Web Forms em 2019 por qualquer motivo.

        protected void RedirectPost(string url, IEnumerable<KeyValuePair<string,string>> fields)
        {
            Response.Clear();

            const string template =
@"<html>
<body onload='document.forms[""form""].submit()'>
<form name='form' action='{0}' method='post'>
{1}
</form>
</body>
</html>";

            var fieldsSection = string.Join(
                    Environment.NewLine,
                    fields.Select(x => $"<input type='hidden' name='{HttpUtility.UrlEncode(x.Key)}' value='{HttpUtility.UrlEncode(x.Value)}'>")
                );

            var html = string.Format(template, HttpUtility.UrlEncode(url), fieldsSection);

            Response.Write(html);

            Response.End();
        }

        private const string JsonDataFieldName = "_jsonData";

        protected void RedirectPost<T>(string url, T bodyPayload)
        {
            var json = JsonConvert.SerializeObject(bodyPayload, Formatting.Indented);
            //explicit type declaration to prevent recursion
            IEnumerable<KeyValuePair<string, string>> postFields = new List<KeyValuePair<string, string>>()
                {new KeyValuePair<string, string>(JsonDataFieldName, json)};

            RedirectPost(url, postFields);

        }

        protected T GetPostData<T>() where T: class 
        {
            var urlEncodedFieldData = Request.Params[JsonDataFieldName];
            if (string.IsNullOrEmpty(urlEncodedFieldData))
            {
                return null;// default(T);
            }

            var fieldData = HttpUtility.UrlDecode(urlEncodedFieldData);

            var result = JsonConvert.DeserializeObject<T>(fieldData);
            return result;
        }
Zar Shardan
fonte
0

Normalmente, tudo que você precisa é transportar algum estado entre essas duas solicitações. Na verdade, há uma maneira realmente divertida de fazer isso, que não depende de JavaScript (pense <noscript />).

Set-Cookie: name=value; Max-Age=120; Path=/redirect.html

Com esse cookie, é possível, na solicitação a seguir /redirect.html recuperar as informações name = value, você pode armazenar qualquer tipo de informação nessa cadeia de pares nome / valor, até 4K de dados (limite de cookie típico). Claro que você deve evitar isso e armazenar códigos de status e bits de sinalizador.

Ao receber esta solicitação, você responderá com uma solicitação de exclusão para esse código de status.

Set-Cookie: name=value; Max-Age=0; Path=/redirect.html

Meu HTTP está um pouco enferrujado. Estou analisando o RFC2109 e o RFC2965 para descobrir o quão confiável isso é, de preferência eu gostaria que o cookie retornasse exatamente uma vez, mas isso não parece ser possível, também, cookies de terceiros pode ser um problema para você se estiver se mudando para outro domínio. Isso ainda é possível, mas não tão indolor quanto quando você está fazendo coisas em seu próprio domínio.

O problema aqui é a simultaneidade, se um usuário avançado estiver usando várias guias e conseguir intercalar algumas solicitações pertencentes à mesma sessão (isso é muito improvável, mas não impossível), isso poderá levar a inconsistências no seu aplicativo.

É a maneira <noscript /> de fazer viagens de ida e volta por HTTP sem URLs sem sentido e JavaScript

Eu forneço esse código como um conceito profundo: se esse código é executado em um contexto que você não conhece, acho que pode descobrir qual parte é o quê.

A idéia é que você chame Relocate com algum estado ao redirecionar, e o URL que você relocou chama GetState para obter os dados (se houver).

const string StateCookieName = "state";

static int StateCookieID;

protected void Relocate(string url, object state)
{
    var key = "__" + StateCookieName + Interlocked
        .Add(ref StateCookieID, 1).ToInvariantString();

    var absoluteExpiration = DateTime.Now
        .Add(new TimeSpan(120 * TimeSpan.TicksPerSecond));

    Context.Cache.Insert(key, state, null, absoluteExpiration,
        Cache.NoSlidingExpiration);

    var path = Context.Response.ApplyAppPathModifier(url);

    Context.Response.Cookies
        .Add(new HttpCookie(StateCookieName, key)
        {
            Path = path,
            Expires = absoluteExpiration
        });

    Context.Response.Redirect(path, false);
}

protected TData GetState<TData>()
    where TData : class
{
    var cookie = Context.Request.Cookies[StateCookieName];
    if (cookie != null)
    {
        var key = cookie.Value;
        if (key.IsNonEmpty())
        {
            var obj = Context.Cache.Remove(key);

            Context.Response.Cookies
                .Add(new HttpCookie(StateCookieName)
                { 
                    Path = cookie.Path, 
                    Expires = new DateTime(1970, 1, 1) 
                });

            return obj as TData;
        }
    }
    return null;
}
John Leidegren
fonte