С недавнего времени занялся разработкой мобильных приложений с помощью 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
    17.12.2017 17:58

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


  1. McElroy
    17.12.2017 21:47

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

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


  1. nomoreload
    18.12.2017 01:14

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


  1. Daar
    18.12.2017 07:26

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


  1. yarosroman
    18.12.2017 08:10

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


  1. mayorovp
    18.12.2017 09:22

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

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


    1. radium
      19.12.2017 01:34

      Рекомендация использовать 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.


  1. Dobby007
    18.12.2017 10:42

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

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