HttpClient, Forms Authentication
От: Qbit86 Кипр
Дата: 19.05.13 12:03
Оценка:
Добрый день, уважаемые коллеги!

Помогите, пожалуйста, разобраться с программной авторизацией на некотором сайте (для примера ниже взят RSDN).
К сожалению в веб-программировании и сетевом взаимодействии разбираюсь слабо, так что возможны фундаментальные бреши в понимании процесса.

Для запуска нижеприведённого кода в LINQPad (C# Statements) нужно подключить (F4) пространства имён System.Net и System.Net.Http из одноимённых сборок.

Используются классы HttpClient, HttpRequestMessage и HttpResponseMessage из .NET 4.5. Документации и примеров по ним негусто.
Не используются классы WebClient, HttpWebRequest и HttpWebResponse из .NET 1.1.

Ожидаемое поведение кода (при правильно введённых логине и пароле): делается POST-запрос к серверу, передавая заполненные input'ы странички аутентификации; сервер возвращает куки, в которых хранится идентификатор сессии; делается GET-запрос какой-нибудь приватной странички, она сохраняется локально.
Фактическое поведение кода: сервер возвращает куки, похожие на идентификатор сессии; но последущее обращение к приватной страничке не авторизует, и возвращает вместо неё страничку логина.

В чём проблема?

var cookieContainer = new CookieContainer();


var authHandler = new HttpClientHandler
{
    UseProxy = false, Proxy = null,
    UseCookies = true, CookieContainer = cookieContainer,
    AllowAutoRedirect = false,
};
var authClient = new HttpClient(authHandler, disposeHandler: true);

HttpContent authRequestContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
    { "lognm", "JaneDoe" },
    { "password", "p@$$w0rd" },
});
var authRequestUri = new Uri(@"http://rsdn.ru/Users/Login.aspx");
using (var authRequest = new HttpRequestMessage(HttpMethod.Post, authRequestUri) { Content = authRequestContent })
using (var authResponse = await authClient.SendAsync(authRequest))
{
    if (authResponse.StatusCode == HttpStatusCode.OK)
    {
        // Среди заголовков есть «Set-Cookie».
        authResponse.Headers.Dump("Auth response headers");
        // В коллекции куки появляются новые значения.
        cookieContainer.GetCookies(new Uri(@"http://rsdn.ru")).Dump("Cookies");
    }
}


var getHandler = new HttpClientHandler
{
    UseProxy = false, Proxy = null,
    UseCookies = true, CookieContainer = cookieContainer,
    AllowAutoRedirect = true,
};
var getClient = new HttpClient(getHandler, disposeHandler: true);

var getRequestUri = new Uri(@"http://rsdn.ru/Users/Private/Favorites.aspx");
using (var getRequest = new HttpRequestMessage(HttpMethod.Get, getRequestUri))
using (var getResponse = await getClient.SendAsync(getRequest))
{
    if (getResponse.StatusCode == HttpStatusCode.OK)
    {
        var dirPath = Path.GetTempPath();
        var fileName = DateTime.Now.TimeOfDay.ToString().Replace(':', '-') + ".html";
        var filePath = Path.Combine(dirPath, fileName);
        using (Stream fs = File.Create(filePath))
        {
            await getResponse.Content.CopyToAsync(fs);
        }
        filePath.Dump("File saved");
    }
}


// To be disposed: authClient, getClient.
Глаза у меня добрые, но рубашка — смирительная!
Re: HttpClient, Forms Authentication
От: andrey82  
Дата: 20.05.13 06:33
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Используются классы HttpClient, HttpRequestMessage и HttpResponseMessage из .NET 4.5. Документации и примеров по ним негусто.

Q>Не используются классы WebClient, HttpWebRequest и HttpWebResponse из .NET 1.1.

Для Web сервера это в общем-то безразлично

Q>В чём проблема?


Посмотреть, как происходит обмен обычного браузера с сайтом и для начала сделать в программе точно такие же заголовки запросов (и при необходимости, доп. запросы).
Перед проверкой почистить кэш и куки у браузера.
Re[2]: POST, Cookies
От: Qbit86 Кипр
Дата: 20.05.13 08:30
Оценка:
Здравствуйте, andrey82, Вы писали:

Q>>Не используются классы WebClient, HttpWebRequest и HttpWebResponse из .NET 1.1.

A>Для Web сервера это в общем-то безразлично

Про это упомянул для того, чтобы предлагаемые решения по возможности опирались на новый API, а не на старый.

A>Посмотреть, как происходит обмен обычного браузера с сайтом...


Мне казалось, что достаточно передать url-encoded содержимое полей логина и пароля. Но, видимо, в POST надо передавать ещё и __VIEWSTATE, или что там в ASP.NET?

A>...и для начала сделать в программе точно такие же заголовки запросов (и при необходимости, доп. запросы).


Ещё, я так понял, есть тонкости работы с HttpOnly-куками, полученными через POST. Куки-контейнер не позволяет напрямую ими манипулировать из соображений безопасности. В сниффере Fiddler они видны, а через предоставляемый API к ним доступ получить нельзя.
Глаза у меня добрые, но рубашка — смирительная!
Re: HttpClient, Forms Authentication
От: samius Япония http://sams-tricks.blogspot.com
Дата: 20.05.13 08:45
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Ожидаемое поведение кода (при правильно введённых логине и пароле): делается POST-запрос к серверу, передавая заполненные input'ы странички аутентификации; сервер возвращает куки, в которых хранится идентификатор сессии; делается GET-запрос какой-нибудь приватной странички, она сохраняется локально.

Q>Фактическое поведение кода: сервер возвращает куки, похожие на идентификатор сессии; но последущее обращение к приватной страничке не авторизует, и возвращает вместо неё страничку логина.

Q>В чём проблема?

Куки не отменяет необходимости посылать логин и пароль серверу при КАЖДОМ запросе.
http://en.wikipedia.org/wiki/Basic_access_authentication#Protocol
Как я понимаю, вообще каждый запрос должен быть сначала отправлен без заголовка авторизации, сервер должен отправить 401 и свой заголовок, клиент должен состряпать заголовок для авторизации и отправить его серверу, только тогда сервер авторизует и возвращает нормальный ответ.
Но многие клиенты кэшируют однажды полученный WWW-Authenticate и не шлют запросов без авторизации после первого раза.
А что касается куки — разработчик может хранить в нем что хочет, но это не должно влиять на протокол.
POST запрос на страничку аутентификации не обязателен. Иначе как бы заходили на форум по ссылке из уведомления по почте?
Т.е. сразу отправляем голый GET для нужной страницы, получаем от сервера 401 и метод аутентификации, пакуем соответственно логин и пароль, отправляем GET с нужным заголовком.
Re[3]: POST, Cookies
От: andrey82  
Дата: 20.05.13 08:51
Оценка:
Здравствуйте, Qbit86, Вы писали:

A>>Посмотреть, как происходит обмен обычного браузера с сайтом...


Q>Мне казалось, что достаточно передать url-encoded содержимое полей логина и пароля. Но, видимо, в POST надо передавать ещё и __VIEWSTATE, или что там в ASP.NET?


В этом-то и состоит смысл сравнения с обменом, которое выполняет браузер. Может быть что-то нетиповое.


A>>...и для начала сделать в программе точно такие же заголовки запросов (и при необходимости, доп. запросы).


Q>Ещё, я так понял, есть тонкости работы с HttpOnly-куками, полученными через POST. Куки-контейнер не позволяет напрямую ими манипулировать из соображений безопасности. В сниффере Fiddler они видны, а через предоставляемый API к ним доступ получить нельзя.


А если использовать более низкоуровневый HttpWebRequest.CookieContainer ? Вроде там доступ ко всем кукам есть.
Re[4]: POST, Cookies
От: Qbit86 Кипр
Дата: 20.05.13 09:05
Оценка:
Здравствуйте, andrey82, Вы писали:

A>А если использовать более низкоуровневый HttpWebRequest.CookieContainer?


Это свойство возвращает экземпляр класса System.Net.CookieContainer — его я и использую. Только в новом API этот контейнер является свойством HttpClient'а (точнее, HttpClientHandler'а), а не конкретного запроса.

A>Вроде там доступ ко всем кукам есть.


Насколько я понимаю — нет. Он втёмную инкапсулирует в себе куки, некоторые из них можно посмотреть через cookieContainer.GetCookies(uri), но в общем случае не предполагается необходимость этого. То есть туда-сюда в API передаётся весь контейнер, а что внутри — специально скрыто от клиентской стороны. Видел в интернетах способ смотреть HttpOnly-куки через интероп с каким-то нативным методом. Но по-хорошему, этого не надо делать, то есть всё должно работать и так. У меня ж не стоит задачи подсунуть в запрос украденные куки; достаточно использовать честно полученные от сервера куки.
Глаза у меня добрые, но рубашка — смирительная!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.