Response.Redirect con POST en lugar de Get?

248

Tenemos el requisito de enviar un formulario y guardar algunos datos, luego redirigir al usuario a una página fuera del sitio, pero al redirigirnos, debemos "enviar" un formulario con POST, no GET.

Esperaba que hubiera una manera fácil de lograr esto, pero estoy empezando a pensar que no la hay. Creo que ahora debo crear una página simple, con solo el formulario que quiero, redirigir a ella, completar las variables del formulario, luego hacer una llamada body.onload a un script que simplemente llama a document.forms [0] .submit ( );

¿Alguien puede decirme si hay una alternativa? Es posible que necesitemos ajustar esto más adelante en el proyecto, y puede ser un poco complicado, por lo que si hubiera una solución fácil, podríamos hacer esto sin depender de otra página, sería fantástico.

De todos modos, gracias por todas y cada una de las respuestas.

Matt Dawdy
fuente
En PHP, puede enviar datos POST con cURL. ¿Hay algo comparable para .NET?
Brian Warshaw
Creo que esta es la respuesta fácil que estabas buscando. No podía creer lo ingenioso que es ... stackoverflow.com/a/6062248/110549
JoeCool
@BrianWarshaw Encuentro que System.Net.Http.HttpClient msdn.microsoft.com/en-us/library/… es muy intuitivo y rápido de usar.
Stoyan Dimov

Respuestas:

228

Hacer esto requiere comprender cómo funcionan las redirecciones HTTP. Cuando lo usa Response.Redirect(), envía una respuesta (al navegador que realizó la solicitud) con el Código de estado HTTP 302 , que le indica al navegador a dónde ir después. Por definición, el navegador lo hará a través de una GETsolicitud, incluso si la solicitud original era a POST.

Otra opción es usar el Código de estado HTTP 307 , que especifica que el navegador debe realizar la solicitud de redireccionamiento de la misma manera que la solicitud original, pero para avisar al usuario con una advertencia de seguridad. Para hacer eso, escribirías algo como esto:

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

Desafortunadamente, esto no siempre funcionará. Diferentes navegadores implementan esto de manera diferente , ya que no es un código de estado común.

Por desgracia, a diferencia de los desarrolladores de Opera y Firefox, los desarrolladores de IE nunca han leído las especificaciones, ¡e incluso el último y más seguro IE7 redirigirá la solicitud POST del dominio A al dominio B sin advertencias ni diálogos de confirmación! Safari también actúa de manera interesante, aunque no genera un diálogo de confirmación y realiza la redirección, arroja los datos POST, cambiando efectivamente la redirección 307 en la 302 más común.

Entonces, hasta donde yo sé, la única forma de implementar algo como esto sería usar Javascript. Hay dos opciones que se me ocurren en la cabeza:

  1. Cree el formulario y haga que su actionatributo apunte al servidor de terceros. Luego, agregue un evento de clic al botón de envío que primero ejecuta una solicitud AJAX a su servidor con los datos, y luego permite que el formulario se envíe al servidor de terceros.
  2. Cree el formulario para publicar en su servidor. Cuando se envía el formulario, muestre al usuario una página que tiene un formulario con todos los datos que desea transmitir, todo en entradas ocultas. Simplemente muestre un mensaje como "Redireccionando ...". Luego, agregue un evento javascript a la página que envía el formulario al servidor de terceros.

De los dos, elegiría el segundo, por dos razones. Primero, es más confiable que el primero porque no se requiere Javascript para que funcione; Para aquellos que no lo tienen habilitado, siempre puede hacer visible el botón de envío para el formulario oculto e indicarles que lo presionen si demora más de 5 segundos. En segundo lugar, puede decidir qué datos se transmiten al servidor de terceros; si usa solo procesa el formulario a medida que pasa, pasará todos los datos de la publicación, que no siempre es lo que desea. Lo mismo para la solución 307, suponiendo que funcionó para todos sus usuarios.

¡Espero que esto ayude!

tghw
fuente
1
Cambie "agregar un evento de clic al botón de envío" a "agregar un evento de envío al formulario". Hay más de un método para enviar el formulario, por ejemplo, presionar Entrar con cualquier entrada de texto enfocada, y mucho menos enviar mediante programación.
temoto
122

Puedes usar este enfoque:

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, justo después de que el cliente obtenga todo el html del servidor, se produce la carga del evento que activa el envío del formulario y publica todos los datos en postbackUrl definido.

Pavlo Neiman
fuente
99
+1 (te agregaría más si pudiera). Esta es la respuesta. Elocuente y al grano. Incluso incluiste todo el código para hacerlo en lugar de hablar de la cabeza. Utilicé esto en un iframe en mi página aspx y renderizó todo perfectamente, sin reescribir URL. ¡Excelente trabajo! CONSEJO para aquellos que usan iframe: apunto mi iframe a otra página aspx que luego ejecuta este código.
MikeTeeVee
1
Funciona! Lo único malo que veo es que si presionas el botón Atrás del navegador, vuelve a hacer la solicitud, por lo que ya no puedes acceder a la página anterior. ¿Hay un lugar de trabajo para eso?
Mt. Schneiders
44
Hay una razón por la cual el método admitido para hacer esto es usar un 307. Las acciones POST están destinadas a ser transacciones idempotentes. Esta respuesta es simplemente un truco que funciona y que los navegadores pueden bloquear fácilmente en el futuro.
Sam Rueby
2
@sam buen punto. Como contraargumento: según la respuesta principal, el desarrollador de IE ni siquiera leyó sobre 307. Otros que lo leyeron lo implementaron incorrectamente. Eso significa que 307 claramente es confuso para la inteligencia (suponiendo que los desarrolladores del navegador sean inteligentes) y propenso a errores de interpretación. El enfoque anterior es claro, al menos para mí, y funciona en todos los navegadores pasados ​​y presentes. No estamos demasiado preocupados por el futuro ya que los desarrolladores luchamos contra el pasado (lea IE7) y la entrada / salida actual. En mi humilde opinión, ya que todos lo entendieron correctamente, deben mantenerlo como está. ¿Cuál sería la razón para bloquearlo en el futuro?
so_mv
2
¿Cómo es que esto no es efectivamente lo mismo que la segunda opción de TGHW? ¿No estás potencialmente pidiendo a alguien descuidado que introduzca una vulnerabilidad XSS?
Scott
33

HttpWebRequest se utiliza para esto.

En la devolución de datos, cree una HttpWebRequest a su tercero y publique los datos del formulario, luego, una vez hecho, puede Response.Redirect donde quiera.

Obtiene la ventaja adicional de que no tiene que nombrar todos los controles de su servidor para hacer el formulario de terceros, puede hacer esta traducción al crear la cadena 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");

Sin embargo, si necesita que el usuario vea la página de respuestas de este formulario, su única opción es utilizar Server.Transfer, y eso puede o no funcionar.

FlySwat
fuente
¿Es lo que voy a usar si quiero un formulario adicional que el formulario asp.net? Necesito publicar algunos datos en una url externa que es para el pago seguro 3D, luego necesito que me devuelvan la información de la solicitud. ¿Es esta la forma de hacer esto? Gracias
Barbaros Alp
Si necesita que el usuario vea la respuesta, puede devolver el contenido y los encabezados correspondientes. Puede que tenga que modificar algunos aspectos del resultado dependiendo del uso de recursos relativos, pero ciertamente es posible.
Thomas S. Trias
66
El usuario puede tener datos de cookies que no se transmitirán a través de este método, ya que esto está sucediendo desde el servidor.
Scott
7

Esto debería hacer la vida mucho más fácil. Simplemente puede usar fácilmente el método Response.RedirectWithData (...) en su aplicación web.

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
fuente
6

Algo nuevo en ASP.Net 3.5 es esta propiedad "PostBackUrl" de los botones ASP. Puede establecerlo en la dirección de la página en la que desea publicar directamente, y cuando se hace clic en ese botón, en lugar de volver a publicar en la misma página como de costumbre, en su lugar publica en la página que ha indicado. Práctico. Asegúrese de que UseSubmitBehavior también esté establecido en TRUE.

Mike K
fuente
4

Pensé que podría ser interesante compartir que heroku hace esto con su SSO para proveedores de complementos

Se puede ver un ejemplo de cómo funciona en la fuente de la herramienta "kensa":

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

Y se puede ver en la práctica si desactiva JavaScript. Fuente de página de ejemplo:

<!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
fuente
3

PostbackUrl se puede configurar en su botón asp para publicar en una página diferente.

si necesita hacerlo en código detrás, intente Server.Transfer.

Palanqueta
fuente
2

@Mate,

Todavía puede usar HttpWebRequest, luego dirigir la respuesta que recibe a la respuesta del flujo de salida real, esto le devolvería la respuesta al usuario. El único problema es que cualquier URL relativa se rompería.

Aún así, eso puede funcionar.

FlySwat
fuente
2

Esto es lo que haría:

Ponga los datos en un formulario estándar (sin atributo runat = "server") y configure la acción del formulario para publicar en la página de destino fuera del sitio. Antes de enviar, enviaría los datos a mi servidor usando una XmlHttpRequest y analizaría la respuesta. Si la respuesta significa que debe continuar con la PUBLICACIÓN fuera del sitio, entonces yo (el JavaScript) procedería con la publicación; de lo contrario, redirigiría a una página en mi sitio

Andrei Rînea
fuente
Esto funciona, pero es importante tener en cuenta que pierde toda la funcionalidad del lado del servidor ASP.NET.
senfo
2

En PHP, puede enviar datos POST con cURL. ¿Hay algo comparable para .NET?

Sí, HttpWebRequest, mira mi publicación a continuación.

FlySwat
fuente
2

El método GET (y HEAD) nunca debe usarse para hacer algo que tenga efectos secundarios. Un efecto secundario podría estar actualizando el estado de una aplicación web, o podría estar cargando su tarjeta de crédito. Si una acción tiene efectos secundarios, se debe utilizar otro método (POST).

Por lo tanto, un usuario (o su navegador) no debe ser considerado responsable de algo hecho por un GET. Si ocurriera algún efecto secundario dañino o costoso como resultado de un GET, eso sería culpa de la aplicación web, no del usuario. Según la especificación, un agente de usuario no debe seguir automáticamente una redirección a menos que sea una respuesta a una solicitud GET o HEAD.

Por supuesto, muchas solicitudes GET tienen algunos efectos secundarios, incluso si solo se agregan a un archivo de registro. Lo importante es que la aplicación, no el usuario, se haga responsable de esos efectos.

Las secciones relevantes de la especificación HTTP son 9.1.1 y 9.1.2 , y 10.3 .

erickson
fuente
1

Sugiero construir una HttpWebRequest para ejecutar programáticamente su POST y luego redirigir después de leer la Respuesta, si corresponde.

Ben Griswold
fuente
1

Código copiable y pegado basado en el método de Pavlo Neyman

RedirectPost (string url, T bodyPayload) y GetPostData () son para aquellos que solo quieren volcar algunos datos fuertemente tipados en la página de origen y recuperarlos en el destino. NewtonSoft Json.NET debe poder serializar los datos y, por supuesto, debe hacer referencia a la biblioteca.

Simplemente copie y pegue en su (s) página (s) o mejor aún, la clase base para sus páginas y úsela en cualquier lugar de su aplicación.

Mi corazón está con todos ustedes que todavía tienen que usar Web Forms en 2019 por cualquier razón.

        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
fuente
0

Por lo general, todo lo que necesitará es llevar algún estado entre estas dos solicitudes. En realidad, hay una forma realmente funky de hacer esto que no se basa en JavaScript (piense en <noscript />).

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

Con esa cookie allí, puede en la siguiente solicitud para /redirect.html recuperar la información nombre = valor, puede almacenar cualquier tipo de información en esta cadena de par nombre / valor, hasta por ejemplo 4K de datos (límite de cookie típico). Por supuesto, debe evitar esto y almacenar códigos de estado y bits de bandera en su lugar.

Al recibir esta solicitud, a cambio, responde con una solicitud de eliminación para ese código de estado.

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

Mi HTTP está un poco oxidado. He estado revisando RFC2109 y RFC2965 para determinar qué tan confiable es realmente, preferiblemente me gustaría que la cookie se redondeara exactamente una vez, pero eso no parece ser posible, también, cookies de terceros. podría ser un problema para usted si se traslada a otro dominio. Esto todavía es posible, pero no tan fácil como cuando estás haciendo cosas dentro de tu propio dominio.

El problema aquí es la concurrencia, si un usuario avanzado está usando múltiples pestañas y logra intercalar un par de solicitudes que pertenecen a la misma sesión (esto es muy poco probable, pero no imposible), esto puede generar inconsistencias en su aplicación.

Es la forma <noscript /> de realizar viajes de ida y vuelta HTTP sin URL y JavaScript sin sentido

Proporciono este código como un concepto de concepto: si este código se ejecuta en un contexto con el que no está familiarizado, creo que puede averiguar qué parte es qué.

La idea es que llame a Relocate con algún estado cuando redirija, y la URL que reubicó llama a GetState para obtener los datos (si corresponde).

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
fuente