Простая реализация Token для взаимодействия мобильного приложения с WebAPI +6



С недавнего времени занялся разработкой мобильных приложений с помощью Xamarin.Forms в связи с производственной так сказать необходимостью. Не буду конечно рассказывать про танцы с бубнами чтобы написать и запустить на эмуляторе приложение «Hello, World!», но главное разработка пошла достаточно плавно.

Благо и понимание задачи было — а именно — взаимодействие мобильного приложения с базой данных внутренней CRM системы в компании, добавить сотрудникам мобильности, но при этом не забывать и о безопасности. Было принято решение создать WebAPI, ибо чтобы работать с уже привычными ASMX веб-сервисами в Xamarin нужно плясать с бубнами.

Как сказал выше, в том числе хотелось сделать «связующее звено» достаточно безопасным, а значит мобильное приложение должно иметь авторизацию (до кучи и удобства с возможностью сохранения авторизации и автоматического входа.

Не хотелось глубоко копаться в реализации WebAPI с авторизацией на уровне Token, а хотелось сделать что-то попроще, тем более пока «гуглил» видел что такого желания у людей с избытком, но все отсылы отвечающих были либо к стандартным механизмам, либо использования каких-нибудь пакетов из NuGet, чего хотелось бы по максимуму избежать.

В базе собственной CRM и так уже есть вся информация для авторизации и городить что-то лишнее тупо не хотелось.

В итоге, после долгих мытарств, поисков и т.п. — думаю у меня получилось достаточно неплохое решение, которым мне и хочется поделится с сообществом.

Итак, что мы имеем:

Авторизация и получение Token


Не буду опять-таки вдаваться в подробности создания WebAPI, приведу пример кода функции авторизации.

[HttpPost]
public LoginResult Logining([FromBody] LoginInfo LogInf)
   {
   if (dc.Managers.Count(w => w.EMail.StartsWith(LogInf.PortalUserEMail)) > 0)
      {
         if (dc.Managers.Count(w => w.EMail.StartsWith(LogInf.PortalUserEMail) && w.Password == LogInf.PortalUserPassword) > 0)
            {
               Managers _man = dc.Managers.First(w => w.EMail.StartsWith(LogInf.PortalUserEMail));
               _man.Token = GenerateToken();
               dc.SubmitChanges();
               return new LoginResult()
                  {
                     Error = null, Token = _man.Token, ValidUser = true,
                     managerInfo = new ManagerInfo() { ManagerId = _man.Id, ManagerName = _man.ManagerName }
                  };
            }
            else
            {
               return new LoginResult()
                  {
                     Error = "Неверный пароль!", Token = null, ValidUser = false, managerInfo = null
                  };
            }
      }
      else
      {
         return new LoginResult()
            {
               Error = "Такого пользователя не существует!", Token = null, ValidUser = false, managerInfo = null
            };
      }
   }

Объяснять что делает функция думаю тоже нет смысла, но как результат при вызове API мобильное приложение получает информацию что юзер валиден и выдаёт сгенерированный Token (я решил не мелочится и заложил генерацию 1000-символьной строки из большого количества символов всей английской и русской клавиатуры, с заглавными и строчными буквами, цифр и простых символов.

Этот «псевдо»-Token я прописываю в
App.Current.Properties["Token"] = rez.Token;
приложения.

Кстати стоит, как мне кажется, отдельно отметить 3 потраченных дня, откаты версий и т.п. чтобы разобраться с этим самым App.Current.Properties.

Произошла ситуация, что в какой-то момент при перезапуске приложения на эмуляторе содержимое App.Current.Properties отсутствовало. Долго мучился и пытался понять почему всё пропадает.

Оказывается пока приложение активно в App.Current.Properties могут хранится любые объекты, в том числе и объекты с данными собственных классов, но при «убиении» процесса если там было что-то отличное от «простых» объектов — содержимое App.Current.Properties отчищается, но если там хранить только простые объекты — string, bool, int и т.п. то всё останется сохраненным!

Продолжим. Все последующие обращения к API я снабжаю дополнительным заголовком:

var client = new HttpClient();
var address = $"http://хх.ххх.х.хх/SomeWebApi/api/Works?ManId=" + ManagerId;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Token", (string)App.Current.Properties["Token"]);
HttpResponseMessage response = await client.GetAsync(address);

Что по сути к заголовкам запроса добавляет хидер. Теперь все остальные контроллеры WebAPI перед тем как что-либо «выдать» клиенту проверяют наличие и соответствие псевдо-Token.

К примеру:

public IEnumerable<WorkInformation> Get([FromUri] int ManId)
   {
      IEnumerable<string> UserToken;
      if (!Request.Headers.TryGetValues("Token", out UserToken))
         {
            return null;
         }
      Managers _CurrManager = dc.Managers.First(w => w.Id == ManId);
      List<WorkInformation> _list = new List<WorkInformation>();
      if (_CurrManager.Token != UserToken.ToArray()[0])
         {
            _list.Add(new WorkInformation() { id = "-1", Client = "Ошибка проверки ключа защиты", DTime = DateTime.Now, Comment = "Перезайдите в приложение", EventImageName = "" });
            return _list;
         }
      //если всё ОК делаем что надо
   }

Как мне кажется полученная реализация псевдо-токена имеет право на существование и кому-то может помочь в реализациях. Надеюсь, сообщество палками меня не закидает, это мой первый пост, надеюсь что будет не только критика.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (8):

  1. Mikluho
    /#10571558

    Ох, зря Вы так… Ведь и правда закидают.
    Ваша реализация крайне небезопасна. С точки зрения механизма, у вас не токен, а сессионная кука.
    Но хуже всего сама аутентификация. Серьёзно, поиск по частичному совпадению логина и пароль с чистом виде?

  2. McElroy
    /#10571838

    использования каких-нибудь пакетов из NuGet, чего хотелось бы по максимуму избежать

    а в чем проблема использования пакетов из NuGet?

  3. nomoreload
    /#10572008

    Для авторизации есть OAuth 2.0.
    Для аутентификации — OpenID Connect 1.0.
    Зачем изобретать изобретённое?
    В случае дотнета — есть IdentityServer, который реализует оба протокола.

  4. Daar
    /#10572110

    Такие штуки я писал лет 15 назад :)))

  5. yarosroman
    /#10572142

    Эх огорчу вас с вашим велосипедом, в ASP.Net (что в Core, что в классическом) уже есть возможность работы с JWT Bearer токенами. В вашем велосипеде придется в каждый акшн добавлять проверку токена, когда при использовании JWT, просто добавить атрибут [Authorize] к контроллеру и все. Плюс, к тому токену можно прицепить кучу инфы(данные пользователя, время действия токена и тд.). А ваше решение даже безопасным нельзя назвать.

  6. mayorovp
    /#10572204

    Зря вы используете нестандартный заголовок HTTP без префикса X-, это может однажды плохо кончиться. Надо или использовать заголовок X-Token вместо Token, или воспользоваться стандартным `Authorization: Bearer ...` либо полустандартным `Authorization: Token ...`.

    Ну и аутентификацию надо делать фильтрами, а не писать один и тот же код в каждом методе.

    • radium
      /#10574224

      Рекомендация использовать X-префиксы устарела в 2012 году с выходом RFC6648.

      Creators of new parameters to be used in the context of application
      protocols:

      1. SHOULD assume that all parameters they create might become
      standardized, public, commonly deployed, or usable across
      multiple implementations.

      2. SHOULD employ meaningful parameter names that they have reason to
      believe are currently unused.

      3. SHOULD NOT prefix their parameter names with «X-» or similar
      constructs.

  7. Dobby007
    /#10572360

    Оказывается пока приложение активно в App.Current.Properties могут хранится любые объекты, в том числе и объекты с данными собственных классов, но при «убиении» процесса если там было что-то отличное от «простых» объектов — содержимое App.Current.Properties отчищается, но если там хранить только простые объекты — string, bool, int и т.п. то всё останется сохраненным!

    Я не работал с Xamarin, но мне кажется, что тут дело в возможности сериализации объектов. Вы не пробовали пометить свои кастомные классы атрибутом Serializable?