Response.Redirect with POST zamiast Get?

248

Wymagamy przesłania formularza i zapisania niektórych danych, a następnie przekierowania użytkownika na stronę poza witryną, ale w przypadku przekierowania musimy „przesłać” formularz za pomocą POST, a nie GET.

Miałem nadzieję, że można to łatwo osiągnąć, ale zaczynam myśleć, że nie ma. Myślę, że muszę teraz utworzyć prostą inną stronę, używając tylko żądanego formularza, przekierować do niego, wypełnić zmienne formularza, a następnie wykonać wywołanie body.onload do skryptu, który wywołuje tylko document.forms [0] .submit ( );

Czy ktoś może mi powiedzieć, czy istnieje alternatywa? Być może będziemy musieli dostosować to później w projekcie, a może to się skomplikować, więc gdyby istniała łatwa możliwość, moglibyśmy zrobić to wszystko zależnie od innych stron, co byłoby fantastyczne.

W każdym razie dzięki za wszelkie odpowiedzi.

Matt Dawdy
źródło
W PHP możesz wysyłać dane POST za pomocą cURL. Czy istnieje coś porównywalnego dla platformy .NET?
Brian Warshaw,
Myślę, że to prosta odpowiedź, której szukałeś. Nie mogłem uwierzyć, jak to jest genialne ... stackoverflow.com/a/6062248/110549
JoeCool
@BrianWarshaw Uważam, że System.Net.Http.HttpClient msdn.microsoft.com/en-us/library/… jest bardzo intuicyjny i szybki w użyciu.
Stoyan Dimov

Odpowiedzi:

228

Wykonanie tego wymaga zrozumienia, jak działają przekierowania HTTP. Gdy korzystasz Response.Redirect(), wysyłasz odpowiedź (do przeglądarki, która wysłała żądanie) z kodem stanu HTTP 302 , który informuje przeglądarkę, gdzie należy przejść dalej. Z definicji przeglądarka zrobi to za pośrednictwem GETżądania, nawet jeśli pierwotne żądanie to POST.

Inną opcją jest użycie kodu stanu HTTP 307 , który określa, że ​​przeglądarka powinna wysłać żądanie przekierowania w taki sam sposób, jak żądanie oryginalne, ale poprosić użytkownika o ostrzeżenie bezpieczeństwa. Aby to zrobić, napiszesz coś takiego:

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

Niestety nie zawsze to działa. Różne przeglądarki implementują to inaczej , ponieważ nie jest to wspólny kod stanu.

Niestety, w przeciwieństwie do programistów Opera i FireFox, programiści IE nigdy nie przeczytali specyfikacji, a nawet najnowszy, najbezpieczniejszy IE7 przekieruje żądanie POST z domeny A do domeny B bez ostrzeżeń lub okien dialogowych z potwierdzeniami! Safari działa również w interesujący sposób, chociaż nie wywołuje okna dialogowego z potwierdzeniem i wykonuje przekierowanie, odrzuca dane POST, skutecznie zmieniając przekierowanie 307 na bardziej powszechne 302.

Tak więc, o ile mi wiadomo, jedynym sposobem na wdrożenie czegoś takiego jest użycie Javascript. Są dwie opcje, które mogę wymyślić z góry głowy:

  1. Utwórz formularz, aby jego actionatrybut wskazywał na serwer innej firmy. Następnie dodaj zdarzenie kliknięcia do przycisku przesyłania, który najpierw wykonuje żądanie AJAX na serwerze z danymi, a następnie umożliwia przesłanie formularza na serwer innej firmy.
  2. Utwórz formularz, aby opublikować na serwerze. Po przesłaniu formularza pokaż użytkownikowi stronę zawierającą formularz ze wszystkimi danymi, które chcesz przekazać, wszystkie w ukrytych danych wejściowych. Po prostu pokaż komunikat „Przekierowanie ...”. Następnie dodaj zdarzenie javascript do strony, która przesyła formularz do serwera innej firmy.

Z dwóch wybrałbym drugi z dwóch powodów. Po pierwsze, jest bardziej niezawodny niż pierwszy, ponieważ Javascript nie jest wymagany do jego działania; dla tych, którzy nie mają włączonej opcji, zawsze możesz wyświetlić przycisk przesyłania ukrytego formularza i poinstruować go, aby go nacisnął, jeśli zajmie to więcej niż 5 sekund. Po drugie, możesz zdecydować, jakie dane zostaną przesłane do serwera innej firmy; jeśli użyjesz tylko formularza w miarę upływu czasu, będziesz przekazywał wszystkie dane postu, co nie zawsze jest tym, czego chcesz. To samo dotyczy rozwiązania 307, zakładając, że działało ono dla wszystkich użytkowników.

Mam nadzieję że to pomoże!

tghw
źródło
1
Zmień „dodaj zdarzenie kliknięcia do przycisku przesłania”, aby „dodać zdarzenie przesłania do formularza”. Istnieje więcej niż jedna metoda przesłania formularza, np. Naciśnięcie klawisza Enter z dowolnym ukierunkowaniem wprowadzania tekstu, nie mówiąc już o programowym przesyłaniu.
temoto
122

Możesz użyć tego podejścia:

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

W wyniku zaraz po klient otrzyma wszystkie HTML z serwera zdarzenie onload nastąpić że wyzwalacze przesyłania formularza i umieszczać wszystkie dane do określonej postbackUrl.

Pavlo Neiman
źródło
9
+1 (dodałbym więcej, gdybym mógł). To jest odpowiedź. Wymowny i na temat. Dołączyłeś nawet cały kod, aby to zrobić, zamiast mówić z czubka głowy. Użyłem tego w ramce iframe na mojej stronie aspx i wszystko renderowało idealnie - bez przepisywania adresów URL. Wspaniała robota! WSKAZÓWKA dla osób używających iframe: wskazuję moją ramkę iframe na inną stronę aspx, która następnie wykonuje ten kod.
MikeTeeVee
1
To działa! Jedyną złą rzeczą, jaką widzę, jest to, że jeśli naciśniesz przycisk Wstecz przeglądarki, wykonuje ona żądanie ponownie, więc tak naprawdę nie możesz już przejść do poprzedniej strony. Czy istnieje na to miejsce do obejścia?
Mt. Schneiders
4
Istnieje powód, dla którego obsługiwaną metodą jest użycie 307. Działania POST mają być transakcjami idempotentnymi. Ta odpowiedź to tylko hack, który działa i może być bardzo łatwo zablokowany przez przeglądarki w przyszłości.
Sam Rueby
2
@sam dobry punkt. Jako argument przeciwny: według najwyższej odpowiedzi deweloper IE nawet nie przeczytał około 307. Inni, którzy go czytali, źle go zaimplementowali. Oznacza to, że 307 jest wyraźnie mylące dla inteligentnych (zakładając, że twórcy przeglądarek są inteligentni) i podatne na błędy interpretacyjne. Powyższe podejście jest jasne, przynajmniej dla mnie i działa we wszystkich przeglądarkach przeszłych i obecnych. Nie martw się zbytnio o przyszłość, ponieważ my programiści walczymy z przeszłością (czytaj IE7) i dzisiejszym wejściem / wyjściem. IMHO, ponieważ wszyscy dobrze to zrozumieli, powinni je zachować bez zmian. Jaki byłby powód, aby to zablokować w przyszłości?
so_mv
2
Jak to nie jest efektywnie takie samo jak druga opcja TGHW? Czy potencjalnie nie poprosisz też kogoś, kto nieostrożnie wprowadzi lukę w XSS?
Scott
33

Do tego służy HttpWebRequest.

Po zakończeniu utwórz HttpWebRequest do swojej strony trzeciej i opublikuj dane formularza, a następnie, gdy to zrobisz, możesz Response.Redirect gdziekolwiek chcesz.

Dodatkową zaletą jest to, że nie trzeba nazywać wszystkich kontrolek serwera, aby utworzyć formularz innej firmy, można wykonać to tłumaczenie podczas budowania łańcucha 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");

Jeśli jednak potrzebujesz, aby użytkownik zobaczył stronę odpowiedzi z tego formularza, jedyną opcją jest skorzystanie z Server.Transfer, a to może, ale nie musi, działać.

FlySwat
źródło
czy tego chcę użyć, jeśli chcę mieć dodatkowy formularz niż formularz asp.net. Muszę opublikować niektóre dane na zewnętrznym adresie URL, który jest przeznaczony do bezpiecznej płatności 3d, a następnie muszę uzyskać informacje zwrócone z żądania. Czy tak to robi? Dziękuję
Barbaros Alp
Jeśli potrzebujesz, aby użytkownik zobaczył odpowiedź, możesz po prostu odesłać zawartość i odpowiednie nagłówki. Być może będziesz musiał zmodyfikować niektóre aspekty wyniku w zależności od wykorzystania względnych zasobów, ale z pewnością jest to możliwe.
Thomas S. Trias,
6
Użytkownik może mieć dane cookie, które nie byłyby przesyłane tą metodą, ponieważ dzieje się tak z serwera.
Scott
7

To powinno ułatwić życie. Możesz po prostu użyć metody Response.RedirectWithData (...) w swojej aplikacji internetowej.

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
źródło
6

Nowością w programie ASP.Net 3.5 jest właściwość „PostBackUrl” przycisków ASP. Możesz ustawić go na adres strony, na której chcesz publikować bezpośrednio, a po kliknięciu tego przycisku zamiast publikować z powrotem na tej samej stronie, jak zwykle, zamiast tego publikuje na wskazanej stronie. Poręczny. Upewnij się, że wartość UseSubmitBehavior jest również ustawiona na PRAWDA.

Mike K.
źródło
4

Pomyślałem, że interesujące może być podzielenie się tym, że heroku robi to z SSO dostawcom dodatków

Przykład tego, jak to działa, można zobaczyć w źródle narzędzia „kensa”:

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

I można to zobaczyć w praktyce, jeśli włączysz JavaScript. Przykładowe źródło strony:

<!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>
Jakub
źródło
3

PostbackUrl można ustawić na przycisku asp, aby publikować na innej stronie.

jeśli musisz to zrobić w codebehind, spróbuj Server.Transfer.

Jimmy
źródło
2

@Matt,

Nadal możesz użyć HttpWebRequest, a następnie przekierować otrzymaną odpowiedź na rzeczywistą odpowiedź strumienia wyjściowego, to odeśle odpowiedź użytkownikowi. Jedynym problemem jest to, że wszelkie względne adresy URL zostałyby zepsute.

Mimo to może to działać.

FlySwat
źródło
2

Oto co bym zrobił:

Umieść dane w standardowej formie (bez atrybutu runat = "server") i ustaw akcję formularza na publikowanie na docelowej stronie zewnętrznej. Przed przesłaniem przesyłam dane na mój serwer za pomocą XmlHttpRequest i analizuję odpowiedź. Jeśli odpowiedź oznacza, że ​​powinieneś zacząć od POSTingu poza witryną, to ja (JavaScript) kontynuowałbym post, w przeciwnym razie przekierowałbym na stronę w mojej witrynie

Andrei Rînea
źródło
To działa, ale ważne jest, aby pamiętać, że tracisz wszystkie funkcje po stronie serwera ASP.NET.
senfo
2

W PHP możesz wysyłać dane POST za pomocą cURL. Czy istnieje coś porównywalnego dla platformy .NET?

Tak, HttpWebRequest, zobacz mój post poniżej.

FlySwat
źródło
2

Metoda GET (i HEAD) nigdy nie powinna być używana do robienia czegokolwiek, co ma skutki uboczne. Skutkiem ubocznym może być aktualizacja stanu aplikacji internetowej lub obciążenie karty kredytowej. Jeśli akcja ma skutki uboczne, należy zastosować inną metodę (POST).

Tak więc użytkownik (lub jego przeglądarka) nie powinien być pociągany do odpowiedzialności za coś zrobionego przez GET. Jeśli w wyniku GET wystąpi jakiś szkodliwy lub kosztowny efekt uboczny, będzie to wina aplikacji internetowej, a nie użytkownika. Zgodnie ze specyfikacją agent użytkownika nie może automatycznie podążać za przekierowaniem, chyba że jest to odpowiedź na żądanie GET lub HEAD.

Oczywiście wiele żądań GET ma pewne skutki uboczne, nawet jeśli tylko dołącza się do pliku dziennika. Ważne jest, aby aplikacja, a nie użytkownik, była odpowiedzialna za te efekty.

Odpowiednie sekcje specyfikacji HTTP to 9.1.1 i 9.1.2 oraz 10.3 .

erickson
źródło
1

Sugeruję zbudowanie HttpWebRequest w celu programowego wykonania testu POST, a następnie przekierowanie po przeczytaniu odpowiedzi, jeśli dotyczy.

Ben Griswold
źródło
1

Kod do wklejenia w oparciu o metodę Pavlo Neymana

RedirectPost (ciąg adresu URL, T bodyPayload) i GetPostData () są przeznaczone dla tych, którzy chcą po prostu zrzucić niektóre silnie wpisane dane na stronie źródłowej i pobrać je z powrotem w docelowej. Dane muszą być serializowane przez NewtonSoft Json.NET i musisz oczywiście odwoływać się do biblioteki.

Po prostu skopiuj i wklej na swojej stronie (stronach) lub jeszcze lepszej klasie bazowej dla swoich stron i użyj jej w dowolnym miejscu w swojej aplikacji.

Moje serce kieruję do was wszystkich, którzy z jakiegokolwiek powodu nadal musicie korzystać z formularzy internetowych.

        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
źródło
0

Zazwyczaj wszystko, czego kiedykolwiek potrzebujesz, to przenieść stan między tymi dwoma żądaniami. Jest naprawdę fajny sposób na zrobienie tego, który nie opiera się na JavaScript (pomyśl <noscript />).

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

Dzięki temu plikowi cookie możesz w następującym żądaniu do /redirect.html pobrać informacje nazwa = wartość, możesz przechowywać dowolny rodzaj informacji w ciągu tej pary nazwa / wartość, na przykład 4K danych (typowy limit plików cookie). Oczywiście należy tego unikać i zamiast tego przechowywać kody stanu i bity flagi.

Po otrzymaniu tego żądania w zamian odpowiedz na żądanie usunięcia tego kodu stanu.

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

Mój HTTP jest nieco zardzewiały. Przechodziłem przez RFC2109 i RFC2965, aby dowiedzieć się, jak naprawdę jest to wiarygodne, najlepiej chciałbym, aby ciasteczko dokładnie raz podróżowało, ale nie wydaje się to możliwe, również pliki cookie innych firm może być dla Ciebie problemem, jeśli przenosisz się do innej domeny. Jest to nadal możliwe, ale nie tak bezbolesne, jak w przypadku wykonywania czynności we własnej domenie.

Problemem jest współbieżność, jeśli zaawansowany użytkownik korzysta z wielu kart i przeplata kilka żądań należących do tej samej sesji (jest to bardzo mało prawdopodobne, ale nie niemożliwe), może to prowadzić do niespójności w aplikacji.

Jest to <noscript /> sposób robienia podróży w obie strony HTTP bez bezsensownych adresów URL i JavaScript

Podaję ten kod jako prof koncept: jeśli ten kod jest uruchamiany w kontekście, którego nie znasz, myślę, że możesz dowiedzieć się, co to za część.

Chodzi o to, że podczas przekierowania wywołujesz Relocate z pewnym stanem, a adres URL, który relokowałeś, wywołuje GetState w celu uzyskania danych (jeśli takie istnieją).

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
źródło