Разработка приложения "Ярядом" под VK mini apps. Часть 1 .Net Core

Приложение разрабатывается, чтобы помочь людям найти других людей, со схожими интересами, чтобы разделить время за любимым занятием/интересом. Проект пока находится на стадии бета тестирования во Вконтакте. Исправляю баги и добавляю того что не хватает. Захотелось отвлечься и написать немного про разработку, писал и понял что разделю на части. Здесь больше про бэкэнд нюансы с которыми столкнулся, и все что пользователь не видит.

Немного про Vk mini apps.

Вк мини апп это сайт, который подгружается в iframe, с возможностью взаимодействия с базовым функционал VK через API. То есть часть функций доступна сразу без имплементации взаимодействия с их API, но для этого нужна библиотека VkBridge. Здорово что есть такая библиотека, правда, как и в случае с API документацией, здесь также есть своя особенность с языком на котором вы ее читаете. По умолчанию у меня выбран английский язык в VK, но во Вконтакте есть небольшой прикол с документацией на разных языках. Более полная информация доступна на русском языке, тогда как некоторые страницы на английском либо вовсе отсутствуют, либо частично заполнены. Так что советую перейти сначала на русский прежде чем смотреть документы. А тут группа vk mini apps со ссылками на документацию и прочее.

Бекэнд

Мне нужно было создать web API для взаимодействия мини приложения с сервером. В плане технологий это .Net Core, EF Core в качестве ORM, тот же Swagger чтобы при разработке другой человек мог понять какие типы запросов существуют, с какими входными и выходными данными нужно работать. Плюс несколько дополнительных библиотек.

Первым делом, для приема запросов только со стороны ВК, можно сказать некая авторизация пользователей с Вконтаке, должна быть реализована проверка параметров при каждом запросе, примеры от vk, и ниже приведен пример для .Net Core. OnActionExecuting метод наследуемый от класса ActionFilterAttribute, т.е. фактически вы создаете свой атрибут, который можно повесить на контроллер.

public static string GetToken(string message, string secret) { secret ??= ""; var encoding = new UTF8Encoding(); var keyByte = encoding.GetBytes(secret); var messageBytes = encoding.GetBytes(message); using var hmacsha256 = new HMACSHA256(keyByte); byte[] hashMessage = hmacsha256.ComputeHash(messageBytes); return Convert .ToBase64String(hashMessage) .Replace('+', '-') .Replace('/', '_') .Replace("=", string.Empty); } public override void OnActionExecuting(ActionExecutingContext actionExecutingContext) { string vkUrl = actionExecutingContext.HttpContext.Request.Headers[Header.VkReferers]; // "Referer" if (!string.IsNullOrWhiteSpace(vkUrl)) { var uri = new Uri(vkUrl); var queryParameters = HttpUtility.ParseQueryString(uri.Query); var orderedKeys = queryParameters.AllKeys.Where(p => p.StartsWith("vk_")).OrderBy(p => p); var orderedQuery = HttpUtility.ParseQueryString(string.Empty); foreach (var key in orderedKeys) { orderedQuery[key] = queryParameters[key]; } var token = HmacHash.GetToken(orderedQuery.ToString(), _appSettings.SecretKey); var valid = token.Equals(queryParameters["sign"]); if (valid) return; } actionExecutingContext.Result = new BadRequestResult(); }

Сам параметр vkUrl можно получить из заголовка “referer”. И отдельно еще можно брать параметр vk_user_id из этого заголовка, чтобы использовать его как идентификатор или делать сверку с ним в случае если вы будете использовать его в свойстве своего объекта.

SecretKey - это секретный ключ вашего мини ВК приложения, его можно найти в настройках (на странице вашего приложения - "Защищённый ключ"), там же можно найти сервисный токен который понадобится, например, при запросе данных о пользователях со стороны сервера.

На стороне сервера еще нужно обязательно сделать защиту от спам запросов. Есть готовая библиотека AspNetCoreRateLimit, с помощью которой можно задать ограничения на частоту запросов по IP или ID клиента. Достаточно будет инициализировать рейт лимиты в Startup методе Configure и ConfigureServices.

// ConfigureServices services.AddMemoryCache(); services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting")); services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>(); services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
// Configure app.UseIpRateLimiting(); app.UseMvc();

Я специалньно добавил еще UseMvc() метод после вызова UseIpRateLimiting() (как впринципе указано в документации), но так же поместил вызовы до инициалтзации свагера, так как иначе UseIpRateLimiting не работает. Сами правила описываются в appsettings раздельно для дебага и релиза.

"IpRateLimiting": { // Limit splitted to different types of http (get, post e.t.c) "EnableEndpointRateLimiting": true, "StackBlockedRequests": false, "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, "GeneralRules": [ { "Endpoint": "*", "Period": "1s", "Limit": 3 }, { "Endpoint": "*", "Period": "1m", "Limit": 20 }, { "Endpoint": "*", "Period": "1h", "Limit": 900 } ] },

Отдельно можно сказать про валидацию. Вообще до этого момента не думал про проверку айдишников на положительное значение, тем не менее здесь стал валидировать модели и минимальное значение id. И конечно стал добавлять проверку на все значения которые так или иначе можно проверить. Ожидаемые значения в массивах и то что все элементы массива уникальны, проверять строковые дату и время на валидность.

В качестве бд PostgreSQL, и так как проект связан с геоданными, то необходимо хранить где-то эти координаты, поэтому к БД можно добавить расширение PostGIS, компонент находится в дополнительном установщике Stack Builder. Это расширение позволит вам производить базовые вычисления, такое как расстояние между точками, прямо в запросе.

Общение с БД идет через EF Core, и помимо основой библиотеки для работы с базой Npgsql.EntityFrameworkCore.PostgreSQL, существуют дополнительная Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite, как раз для расширения PostGIS. В ней присутствуют основные типы данных которые есть в запросах с Entity. В моем случае используются гео координаты, но расширение позволяет хранить координаты не только двух точек, но и 3, то есть точку в объеме, поэтому важно задать какие типы точек будут использоваться.

При инициализации приложения можно создать фабрику по производству гео координат и добавить как синглтон в IoC.

// 4326 refers to WGS 84, a standard used in GPS and other geographic systems. var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); // To use single factory when we need to create some point services.AddSingleton(geometryFactory);

А так же не забудьте указать тип, из возможных типов данных БД, в аннотации к свойству типа Point.

HasColumnType("geography (point)")

Важный момент с использованием PostGIS, а точнее с установкой дополнительных расширений на базу данных с использованием миграций. Есть проблема с типами данных которые не обновляются при очередной миграции, саму проблему можно найти здесь. Чтобы предотвратить ошибки возникающие при запросах к БД, можно добавить следующий код в класс DbContext для обновления кэша используемых типов и вызвать этот метод при страте приложения.

public void MigrateDatabase() { if (Database.GetPendingMigrations().Any()) { Database.Migrate(); // Need to reload postgis types, cause of some weird behaviour Database.OpenConnection(); ((NpgsqlConnection)Database.GetDbConnection()).ReloadTypes(); Database.CloseConnection(); } }

Что касаемо использования каких-то функций Вконтакте, типа отправки уведомлений или получение информации о пользователях, я создал отдельную небольшую библиотеку VkApi. Здесь как раз и понадобится Service token из настроек вашего приложения на странице VK. В коде поле называется _accessToken, так как параметр носит такое же название - access_token, именно в него нужно передавать Service token. Не все методы API доступны для вызова с сервисным ключом, будьте внимательны. В основном классе VkApi добавил метод SendNotificationAsync, чтобы вызывать Вкашный notifications.sendMessage, само название VK методов хранится пока в enum - VkApiMethod.

public async Task<NotificationResponse> SendNotificationAsync(long[] usersIds, string message) { if (string.IsNullOrEmpty(message)) throw new ArgumentNullException(nameof(message)); if (message.Length > 254) throw new ArgumentOutOfRangeException(nameof(message)); var queryString = HttpUtility.ParseQueryString(string.Empty); var users = string.Join(",", usersIds); queryString["user_ids"] = users; queryString["message"] = message; queryString["v"] = ApiVersion; queryString["access_token"] = _accessToken; var postValues = new FormUrlEncodedContent(queryString.AllKeys.ToDictionary(k => k, k => queryString[k])); var response = await _httpClient .PostAsync($"{_apiUrl}{VkApiMethod.NotificationsSendMessage.GetDescription()}?{queryString}", postValues) .ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var notificationResponse = JsonConvert.DeserializeObject<NotificationResponse>(json); return notificationResponse; }

Описание API методов ВК можно найти на официальном сайте, перед описанием метода вы можете увидеть фразу: "Этот метод можно вызвать с сервисным ключом доступа". Как раз их возможно вызывать с сервисным ключом. Так же внимательно смотрите на ограничения, как по отправке уведомлений так и других вызовов функций. После каждого запроса от вк приходит ответ ввиде json, в предыдущем методе это класс NotificationResponse. Впринципе у большинства ответов есть пару схожих свойств, поэтому у меня есть базовый класс BaseResponse<TResponse>.

public class BaseResponse<TResponse> { [JsonProperty("response")] public TResponse Response { get; set; } [JsonProperty("error")] public Error Error { get; set; } } public class NotificationResponseModel { [JsonProperty("status")] public bool Status { get; set; } [JsonProperty("user_id")] public long UserId { get; set; } }

Перед каждой отправкой уведомления, необходимо удостоверится включены ли уведомления у пользователя, с помощью еще одного метода apps.isNotificationsAllowed.

public async Task<NotificationAllowanceResponse> IsNotificationsAllowedAsync(long usersId) { var queryString = HttpUtility.ParseQueryString(string.Empty); queryString["user_id"] = usersId.ToString(); queryString["v"] = ApiVersion; queryString["access_token"] = _accessToken; var response = await _httpClient .GetAsync($"{_apiUrl}{VkApiMethod.IsNotificationsAllowed.GetDescription()}?{queryString}") .ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var notificationAllowanceResponse = JsonConvert.DeserializeObject<NotificationAllowanceResponse>(json); return notificationAllowanceResponse; }

Логика для отправки примерно должна быть такой:

  • ищем всех пользователей кому нужно отправить сообщение
  • делаем проверку apps.isNotificationsAllowed
  • группируем пользователей по сообщению, которое хотим отправить, дабы сделать как можно меньше запросов
  • и отправляем сообщение до 100 пользователям сразу.

Дебаг

Все запросы с ВК приложения должны быть защищенными, то есть начинаться с https. Поэтому чтобы произвести отладку требуется какой-нибудь прокси сервис, например, такой как ngrok, который создает временный глобальный адрес с защищенным соединением к нашему локальному API. Вам нужно лишь сначала запустить ваш Web API, а потом запустить ngrok c параметрами: "ngrok http 3033" (3033 - это порт вашего приложения). Подробнее про сам ngrok и его настройку здесь.

P.S.

Если кому-то будет интересно помочь покодить, напишите, правда пока все только за плюсик в карму 😊. Следующую часть напишу попозже и добавлю сюда ссылку.

0
2 комментария
Eugene G

Тоже пишу VK Mini-Apps на .Net Core. На фронте Angular.

Спасибо за статью, подчерпнул полезное про Ngrok и про Rate Limit.
Надеюсь, будет вторая часть

Ответить
Развернуть ветку
Роман Булычев

В статье неверный способ токенизации
"Разработчики зачастую допускают ошибку, используя неявный и интуитивно непонятный explicit-метод передачи, — прикрепляемый браузером заголовок Referer, совпадающий с текущим адресом страницы.

Стоит запрещать браузеру прикреплять этот заголовок, иначе при запросе на какой-либо сторонний сервер вы можете, сами того не подозревая, передать ему свои параметры запуска. После этого злоумышленник получит возможность представиться вашему серверу другим пользователем, используя его аутентификационные данные."

Ответить
Развернуть ветку
-1 комментариев
Раскрывать всегда