|
|
@@ -2,29 +2,21 @@
|
|
|
:----------------:|:----------:|:----------------:
|
|
|
[Аутентификация и авторизация](./api_auth.md) | [Содержание](../readme.md#разработка-своего-api) |
|
|
|
|
|
|
-# HTTP запросы в C#. Получение списка материалов выбранного продукта. Авторизация.
|
|
|
+# HTTP запросы в C#.
|
|
|
|
|
|
* [HTTP-запросы (класс **HttpClient**)](#http-запросы)
|
|
|
* [GET-запрос](#get-запрос)
|
|
|
* [Разбор JSON ответа.](#разбор-json-ответа)
|
|
|
- * [DELETE (Удаление записей)](#delete-удаление-записей)
|
|
|
* [POST запросы с JSON (Добавление записей в модель в терминологии REST API)](#post-запросы-с-json-добавление-записей-в-модель-в-терминологии-rest-api)
|
|
|
-* [Авторизация](#авторизация)
|
|
|
- * [Реализация конечной точки в АПИ](#реализация-конечной-точки-в-апи)
|
|
|
- * [Авторизация в клиентском приложении](#авторизация-в-клиентском-приложении)
|
|
|
|
|
|
-Возвращаемся к проекту на C#.
|
|
|
+Возвращаемся к основному проекту (список продукции).
|
|
|
|
|
|
-Мы остановились на списке материалов.
|
|
|
-
|
|
|
-Реализуем получение списка материалов выбранного продукта с помощью HTTP-запроса.
|
|
|
-
|
|
|
->Напоминаю, в лекции про АПИ было задание реализовать конечную точку `GET /material/{productId:int}`:
|
|
|
+Реализуем получение данных не через базу, а через АПИ с помощью HTTP-запросов.
|
|
|
|
|
|
На C# нам надо решить две задачи:
|
|
|
|
|
|
-* получить JSON-строку с помощью GET-запроса
|
|
|
-* распарсить JSON-строку, получив массив материалов
|
|
|
+* получить JSON-строку с помощью HTTP-запроса
|
|
|
+* распарсить JSON-строку
|
|
|
|
|
|
## HTTP-запросы
|
|
|
|
|
|
@@ -57,264 +49,86 @@ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basi
|
|
|
|
|
|
>[Асинхронность в C#](./cs_async_await.md)
|
|
|
|
|
|
-Я для облегчения вашей работы нарисовал реализацию:
|
|
|
-
|
|
|
-```cs
|
|
|
-// в параметрах URL
|
|
|
-private Task<string> GetBody(string url){
|
|
|
- var basic = Convert.ToBase64String(
|
|
|
- ASCIIEncoding.ASCII.GetBytes("esmirnov:111103"));
|
|
|
-
|
|
|
- var client = new HttpClient();
|
|
|
-
|
|
|
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
|
|
|
+Но можно не заморачиваться и использовать свойство _Result_, которое вернет **синхронный** результат.
|
|
|
|
|
|
- return client.GetStringAsync(url);
|
|
|
-}
|
|
|
-```
|
|
|
+Напишем класс **ApiDataPrivider**:
|
|
|
|
|
|
-Метод *GetStringAsync* возвращает объект `Task<string>`, т.е. задачу, которая выполняется в отдельном потоке и по завершении вернёт строку (тело ответа)
|
|
|
+>Можно как обычно реализовать его "с нуля", реализуя интерфейс **IDataProvider** (в этом случае придется сразу писать все методы), но можно реализовать механизм наследования: сделать потомка класса **DBDataProvider**, постепенно переопределяя методы.
|
|
|
|
|
|
-### Разбор JSON ответа.
|
|
|
+Ниже показана реализация одного метода класса, остальные перепишите сами. И не забудьте в конструкторе главного окна поменять инициализацию _dataProvider_: `Global.dataProvider = new ApiDataProvider();`
|
|
|
|
|
|
-Для работы с JSON нужно установить **NuGet** пакет `Newtonsoft.Json` и использовать метод десериализации объекта:
|
|
|
+>В базовом классе (**DBDataProvider**) нужно добавить методам модификатор **virtual**
|
|
|
|
|
|
```cs
|
|
|
-// в месте, где нам нужно распарсить ответ сервера
|
|
|
-materialList = JsonConvert.DeserializeObject<Material[]>("тут ответ вашего АПИ");
|
|
|
-```
|
|
|
-
|
|
|
-В общем случае может быть десериализован любой валидный JSON, но мы явно в угловых скобках указываем, что ожидаем массив материалов.
|
|
|
-
|
|
|
-Весь метод получения списка материалов помещается в 3 строки:
|
|
|
-
|
|
|
-```cs
|
|
|
-private async void GetMaterials()
|
|
|
+public class ApiDataProvider: DBDataProvider
|
|
|
{
|
|
|
- var resp = await GetBody(
|
|
|
- $"http://localhost:8080/material/{currentProduct.Id}");
|
|
|
+ private static string baseUrl = "http://localhost:5000";
|
|
|
+ public override IEnumerable<Product> getProduct(int pageNum)
|
|
|
+ {
|
|
|
+ var client = new HttpClient();
|
|
|
|
|
|
- materialList = JsonConvert
|
|
|
- .DeserializeObject<Material[]>(resp);
|
|
|
+ var body = client.GetStringAsync(
|
|
|
+ $"{baseUrl}/product?pageNum={pageNum}"
|
|
|
+ ).Result;
|
|
|
|
|
|
- Invalidate("materialList");
|
|
|
+ return JsonConvert.DeserializeObject<Product[]>(body);
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-Так как метод *GetBody* возвращает задачу (**Task**), то мы ставим перед ним ключевое слово **await**, то есть ждём завершения задачи, а сам метод помечаем как асинхронный (**async**), чтобы система знала, что этот метод асинхронный.
|
|
|
-
|
|
|
-### DELETE (Удаление записей)
|
|
|
-
|
|
|
-Для вызова http-метода `DELETE` используется метод *DeleteAsync*:
|
|
|
+Если вы используете авторизацию, то при запросе данных добавляйте заголовок:
|
|
|
|
|
|
```cs
|
|
|
-// тут, как в GetBody, создайте клиента и задайте заголовок
|
|
|
-
|
|
|
-client.DeleteAsync(
|
|
|
- $"http://localhost:8080/material/{productId}/{materialId}");
|
|
|
-```
|
|
|
-
|
|
|
-При работе с таблицей **ProductMaterial** не забываем, что у нас составной первичный ключ и нужны идентификаторы и продукта и материала.
|
|
|
-
|
|
|
-### POST запросы с JSON (Добавление записей в модель в терминологии REST API)
|
|
|
-
|
|
|
-1. В проекте нарисуйте форму добавления материала в текущий продукт (в окне продукции)
|
|
|
-
|
|
|
-1. Реализуйте отправку POST-запроса, который добавит новый материал в таблицу **ProductMaterial**
|
|
|
-
|
|
|
- ```cs
|
|
|
- // при добавлении материала
|
|
|
- // у вас должен быть выбран материал
|
|
|
- // и указано его количество
|
|
|
-
|
|
|
- // сначала запихиваем объект в JSON-строку.
|
|
|
- var jsonString = JsonConvert.SerializeObject(
|
|
|
- new {
|
|
|
- MaterialId = Id выбранного материала,
|
|
|
- Count = количество
|
|
|
- });
|
|
|
-
|
|
|
- // создаём контент для http-запроса
|
|
|
- var json = new StringContent(
|
|
|
- jsonString,
|
|
|
- Encoding.UTF8,
|
|
|
- "application/json");
|
|
|
-
|
|
|
- // и вызываем POST-запрос
|
|
|
- var client = new HttpClient();
|
|
|
-
|
|
|
- // не забываем добавить авторизацию
|
|
|
- client.DefaultRequestHeaders.Authorization =
|
|
|
- new AuthenticationHeaderValue("Basic", basic);
|
|
|
-
|
|
|
- var result = client.PostAsync(
|
|
|
- $"http://localhost:8080/material/{currentProduct.Id}",
|
|
|
- json);
|
|
|
- ```
|
|
|
-
|
|
|
-## Авторизация
|
|
|
-
|
|
|
-Часто в встречается задача сделать авторизацию пользователя и скрыть часть информации для неавторизованного пользователя.
|
|
|
-
|
|
|
-Скрытие сделать просто - сделайте привязку (binding) атрибута _IsVisible_ к какому-нибудь свойству (мы это уже делали для кнопки массовой смены цены продукции).
|
|
|
-
|
|
|
-Показать форму с полями ввода для `логина/пароля` вы тоже уже можете.
|
|
|
-
|
|
|
-Осталось разобраться как отправить запрос авторизации на сервер и обработать его на сервере...
|
|
|
-
|
|
|
-Если отправлять обычным POST-запросом с телом запроса в JSON-формате, то вы это тоже уже умеете. Но для авторизации часто используется старый формат запроса: `application/x-www-form-urlencoded`.
|
|
|
-
|
|
|
-Запрос с таким форматом выглядит примерно так:
|
|
|
-
|
|
|
-```
|
|
|
-POST /login
|
|
|
-Content-Type: application/x-www-form-urlencoded
|
|
|
-
|
|
|
-login=qwerty
|
|
|
-&password=asdf
|
|
|
-```
|
|
|
-
|
|
|
-т.е. параметры запроса формируются как _querystring_, но передаются в теле запроса.
|
|
|
-
|
|
|
-### Реализация конечной точки в АПИ
|
|
|
+var basic = Convert.ToBase64String(
|
|
|
+ ASCIIEncoding.ASCII.GetBytes("esmirnov:123456"));
|
|
|
+
|
|
|
+var client = new HttpClient();
|
|
|
|
|
|
-```cs
|
|
|
-app.MapPost(
|
|
|
- "/login",
|
|
|
- async (context) =>
|
|
|
- {
|
|
|
- /*
|
|
|
- параметры в таком формате автоматически не парсятся
|
|
|
- приходится их вручную доставать из тела запроса
|
|
|
- */
|
|
|
- var formData = await context.Request.ReadFormAsync();
|
|
|
-
|
|
|
- var login = formData["login"].ToString();
|
|
|
- var password = formData["password"].ToString();
|
|
|
-
|
|
|
- /*
|
|
|
- "Успешность" авторизации определяем кодом ответа
|
|
|
- у меня, как обычно, логин и пароль прибиты гвоздями
|
|
|
- но в реальном приложении надо читать из базы
|
|
|
- */
|
|
|
- if (login == "admin" && password == "password")
|
|
|
- {
|
|
|
- /*
|
|
|
- Тут можно передать не только код,
|
|
|
- но и информацию о пользователе
|
|
|
- */
|
|
|
- context.Response.StatusCode = 200;
|
|
|
- }
|
|
|
- else
|
|
|
- context.Response.StatusCode = 401;
|
|
|
- }).AllowAnonymous();
|
|
|
+client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
|
|
|
```
|
|
|
|
|
|
-### Авторизация в клиентском приложении
|
|
|
+Метод *GetStringAsync* возвращает объект `Task<string>`, т.е. **задачу**, которая выполняется в отдельном потоке и по завершении вернёт строку (тело ответа). Чтобы не возиться с асинхронностью, можно запросить свойство _Result_.
|
|
|
|
|
|
->Кнопку "Авторизоваться" и окно авторизации сделайте сами
|
|
|
+### Разбор JSON ответа.
|
|
|
|
|
|
-Для хранения информации об авторизации заведем класс **Globals** со статическими свойствами:
|
|
|
+Для работы с JSON нужно установить **NuGet** пакет `Newtonsoft.Json` и использовать метод десериализации объекта:
|
|
|
|
|
|
```cs
|
|
|
-public class Globals
|
|
|
-{
|
|
|
- public static string? token = null;
|
|
|
-
|
|
|
- public static bool isAuth() => token != null;
|
|
|
-
|
|
|
- public static void createToken(string login, string password)
|
|
|
- {
|
|
|
- token = Convert.ToBase64String(
|
|
|
- ASCIIEncoding.ASCII.GetBytes($"{login}:{password}"));
|
|
|
- }
|
|
|
-}
|
|
|
+return JsonConvert.DeserializeObject<Product[]>(body);
|
|
|
```
|
|
|
|
|
|
-У нас остаётся базовая авторизация и я сразу формирую токен в нужном формате. Если используется авторизация по токену (**Bearer**), то метод _createToken_ не нужен, просто сохранить токен: `Globals.token = "токен, полученный при авторизации"`.
|
|
|
+В общем случае может быть десериализован любой валидный JSON, но мы явно в угловых скобках указываем, что ожидаем массив продуктов.
|
|
|
|
|
|
-И в окне авторизации реализуем отправку запроса:
|
|
|
+### POST запросы с JSON (Добавление записей в модель в терминологии REST API)
|
|
|
|
|
|
```cs
|
|
|
-private async void AuthButton_OnClick(object? sender, RoutedEventArgs e)
|
|
|
-{
|
|
|
- /*
|
|
|
- создаем словарь
|
|
|
- и записываем в него данные в виде пары ключ/значение
|
|
|
- для кадого передаваемого параметра
|
|
|
- */
|
|
|
- var param = new Dictionary<string, string>
|
|
|
- {
|
|
|
- { "login", LoginTextBox.Text },
|
|
|
- { "password", PasswordTextBox.Text }
|
|
|
- };
|
|
|
- var client = new HttpClient();
|
|
|
-
|
|
|
- /*
|
|
|
- В инициализатор запроса добавляется содержимое
|
|
|
- */
|
|
|
- var req = new HttpRequestMessage(
|
|
|
- HttpMethod.Post,
|
|
|
- "http://localhost:8080/login")
|
|
|
- {
|
|
|
- Content = new FormUrlEncodedContent(param)
|
|
|
- };
|
|
|
-
|
|
|
- var response = await client.SendAsync(req);
|
|
|
-
|
|
|
- /*
|
|
|
- При успешной авторизации сохраняем токен
|
|
|
- */
|
|
|
- if (response.StatusCode == HttpStatusCode.OK)
|
|
|
- {
|
|
|
- Globals.createToken(
|
|
|
- LoginTextBox.Text,
|
|
|
- PasswordTextBox.Text);
|
|
|
-
|
|
|
- /*
|
|
|
- тут закрываем окно авторизации,
|
|
|
- а в главном окне обновляем состояние
|
|
|
- визуальных элементов доступных только
|
|
|
- авторизованному пользователю
|
|
|
- */
|
|
|
- }
|
|
|
- /*
|
|
|
- в else можно написать логику для неуспешной авторизации,
|
|
|
- например, показать окно
|
|
|
- */
|
|
|
-
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
->В коде используются свойства *LoginTextBox* и *PasswordTextBox* - это имена соответствующих визуальных элементов в окне авторизации.
|
|
|
+// сначала запихиваем объект в JSON-строку.
|
|
|
+var jsonString = JsonConvert.SerializeObject(product)
|
|
|
|
|
|
-Ну и во всех запросах, требующих авторизации, вставляем токен авторизации:
|
|
|
+// создаём контент для http-запроса
|
|
|
+var json = new StringContent(
|
|
|
+ jsonString,
|
|
|
+ Encoding.UTF8,
|
|
|
+ "application/json"
|
|
|
+);
|
|
|
|
|
|
-```cs
|
|
|
-private Task<string> GetBody(string url)
|
|
|
-{
|
|
|
- var client = new HttpClient();
|
|
|
+// и вызываем POST-запрос
|
|
|
+var client = new HttpClient();
|
|
|
|
|
|
- client.DefaultRequestHeaders.Authorization =
|
|
|
- new AuthenticationHeaderValue(
|
|
|
- "Basic",
|
|
|
- Globals.token // берём готовый
|
|
|
- );
|
|
|
+// не забываем добавить авторизацию
|
|
|
+client.DefaultRequestHeaders.Authorization =
|
|
|
+ new AuthenticationHeaderValue("Basic", basic);
|
|
|
|
|
|
- return client.GetStringAsync(url);
|
|
|
-}
|
|
|
+var result = client.PostAsync(
|
|
|
+ $"{baseUrl}/product",
|
|
|
+ json).Result;
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
**Задание:**
|
|
|
|
|
|
-* реализовать авторизацию:
|
|
|
- - доработка АПИ
|
|
|
- - окно с вводом логина/пароля и авторизация
|
|
|
- - скрытие кнопок "создать продукт" и "сменить цену"
|
|
|
- - не открывать окно редактирование продукции, если пользователь не авторизован
|
|
|
-* реализовать в окне редактирования продукции CRUD для списка материалов продукта, используя HTTP-запросы
|
|
|
+Полностью реализовать класс **ApiDataProvider**
|
|
|
|
|
|
Предыдущая лекция | | Следующая лекция
|
|
|
:----------------:|:----------:|:----------------:
|