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

  • Легкая масштабируемость
  • Минимальная потребность в управлении состоянием сервера
  • Упрощенные и несвязанные службы аутентификации
  • Защита от CSRF-атак

Очевидно, что еще одной причиной популярности подхода на основе токенов является неотъемлемая поддержка распределенной и облачной инфраструктуры. Подход на основе токенов решает проблему традиционного подхода, при котором сервер должен хранить идентификаторы сеанса и соответствующие данные для каждого человека. Одним из подходов, основанных на токенах, является Открытый стандарт на основе JSON (RFC 7519), известный как JSON Web Token. (ЮВТ)

Что такое ЮВТ?

JSON Web Token (JWT) — это подход к безопасной передаче данных по каналу связи. Для аутентификации и авторизации он использует метод передачи токенов с цифровой подписью. JWT состоит из трех частей: заголовка, полезных данных и подписи.
Заголовок используется для идентификации используемого алгоритма подписи и выглядит следующим образом:

{ “alg”: “HS256”, “typ”: “JWT”}

Полезная нагрузка выглядит так:

{ “Name”: “Anjani singh”,”Admin”: “true”,”iat”: “146565644”}

Подпись создается заголовком и полезной нагрузкой кодирования Base64 как:

data = encoded( Header ) + “.” + encoded( Payload ) signature = HMACSHA256 (data, secret key);

Более подробную информацию о JWT можно найти на https://jwt.io/

JWT в теории

Процесс аутентификации JWT можно разбить на следующие 4 этапа:

  1. Пользователь проверяется по базе данных, и претензии генерируются на основе роли пользователя.
  2. Полезная нагрузка, содержащая утверждения или другие данные, связанные с пользователем, подписывается ключом для создания токена и передается обратно пользователю.
  3. Пользователь отправляет этот токен с каждым запросом, обычно в заголовке или файлах cookie, а затем полученный токен расшифровывается для проверки претензии.
  4. Как только пользователь идентифицирован, ему разрешается доступ к серверу ресурсов на основании его заявления.

Преимущество парадигмы аутентификации на основе токенов заключается в том, что вместо хранения информации, связанной с аутентификацией или авторизацией, связанной с каждым пользователем в сеансе, на авторизующем сервере/службе хранится один ключ подписи. Задачу авторизации можно делегировать любому серверу, что делает ее полностью независимой. Пользователи идентифицируются путем проверки утверждений, которые были созданы на первом этапе на основе его/ее разрешения. Заявкам можно доверять, поскольку они были сгенерированы сервером на первом этапе, а затем подписаны цифровой подписью с использованием одного из алгоритмов, например HMAC SHA256. Также гарантируется, что права или требования не были подделаны. Уникальная вещь здесь, которая экономит много памяти и повышает масштабируемость, заключается в том, что на сервере требуется только один ключ для расшифровки токена и идентификации пользователя, независимо от того, какое количество пользователей он поддерживает.

После того, как идентификация выполнена, личность текущего пользователя должна сохраняться на протяжении всего запроса. Здесь каждая реализация может отличаться. В следующем разделе рассматриваются все четыре шага, связанные с использованием маркера JWT с веб-API ASP.NET.

Реализация JWT с веб-API Asp.Net

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

Я использовал библиотеку System.IdentityModel.Tokens.Jwt для создания и проверки токенов.
Чтобы реализовать JWT в веб-API, я создал фильтр для аутентификации, который будет выполняться перед каждым запросом. Он проверит токен, содержащийся в заголовке запроса, и откажет/разрешит ресурс на основе токена. Фильтр выглядит следующим образом:

public class JWTAuthenticationFilter : AuthorizationFilterAttribute
    {

        public override void OnAuthorization(HttpActionContext filterContext)
        {

            if (!IsUserAuthorized(filterContext))
            {
                ShowAuthenticationError(filterContext);
                return;
            }
            base.OnAuthorization(filterContext);
        }
}

Я изменил OnAuthorizationметод AuthorizationFilter и внедрил логику для проверки токена. (Обратите внимание, что при запуске MVC 5 также доступен фильтр аутентификации)
Прежде чем я подробно расскажу о том, как работает метод IsUserAuthorized , который содержит всю логику, позвольте мне представить модуль аутентификации, который выполняет всю работу, связанную с этим. для расшифровки, шифрования, подписи и проверки токена.

AuthenticationModule — это место, где используется загруженная библиотека. В нескольких блогах есть различные примеры использования библиотек. Этот ответ stackoverflow также дает очень хорошее объяснение. Следовательно, я просто вставляю свои фрагменты кода, разработанные после необходимых изменений.

public class AuthenticationModule
    {
  private const string communicationKey = "GQDstc21ewfffffffffffFiwDffVvVBrk";
        SecurityKey signingKey = new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes(communicationKey));

 // The Method is used to generate token for user
        public string GenerateTokenForUser(string userName, int userId)
        {

            var signingKey = new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes(communicationKey));
            var now = DateTime.UtcNow;
            var signingCredentials = new SigningCredentials(signingKey,
               SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest);

            var claimsIdentity = new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.Name, userName),
                new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
            }, "Custom");

            var securityTokenDescriptor = new SecurityTokenDescriptor()
            {
                AppliesToAddress = "http://www.example.com",
                TokenIssuerName = "self",
                Subject = claimsIdentity,
                SigningCredentials = signingCredentials,
                Lifetime = new Lifetime(now, now.AddYears(1)),
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            var plainToken = tokenHandler.CreateToken(securityTokenDescriptor);
            var signedAndEncodedToken = tokenHandler.WriteToken(plainToken);

            return signedAndEncodedToken;

        }

        /// Using the same key used for signing token, user payload is generated back
        public JwtSecurityToken GenerateUserClaimFromJWT(string authToken)
        {
      
            var tokenValidationParameters = new TokenValidationParameters()
            {
                ValidAudiences = new string[]
                      {
                    "http://www.example.com",
                      },

                ValidIssuers = new string[]
                  {
                      "self",
                  },
                IssuerSigningKey = signingKey
            };
            var tokenHandler = new JwtSecurityTokenHandler();

            SecurityToken validatedToken;

            try {

              tokenHandler.ValidateToken(authToken,tokenValidationParameters, out validatedToken);
            }
            catch (Exception)
            {
                return null;

            }
    
            return validatedToken as JwtSecurityToken;

        }

    private JWTAuthenticationIdentity PopulateUserIdentity(JwtSecurityToken userPayloadToken)
        {
            string name = ((userPayloadToken)).Claims.FirstOrDefault(m => m.Type == "unique_name").Value;
            string userId = ((userPayloadToken)).Claims.FirstOrDefault(m => m.Type == "nameid").Value;
            return new JWTAuthenticationIdentity(name) { UserId = Convert.ToInt32(userId), UserName = name };

        }
}

Модуль аутентификации содержит три метода:

1) GenerateTokenForUser используется для создания утверждения, когда пользователь является подлинным. Этот метод содержит всю информацию, которую мы хотим передавать туда и обратно, и использует секретный ключ для подписи на основе указанного алгоритма. Зашифрованной информацией может быть имя пользователя, идентификатор пользователя, роли, время истечения срока действия и т. д.
2) GenerateUserClaimFromJWTполучает токен из заголовка и расшифровывает его для получения утверждений.< br /> 3) PopulateUserIdentityиспользуется для создания объекта удостоверения после получения информации из утверждений с использованием библиотеки. Я создал объект JWTAuthenticationIdentity, производный от GenericIdentity пространства имен System.Security.Principal. Это сделано для того, чтобы я мог хранить дополнительную информацию, такую ​​как роль, идентификатор пользователя и т. д. Класс выглядит так:

public class JWTAuthenticationIdentity : GenericIdentity
    {
        
        public string UserName { get; set; }
        public int UserId { get; set; }

        public JWTAuthenticationIdentity(string userName)
            : base(userName)
        {
            UserName = userName;
        }

    }

При первой регистрации я вызвал метод GenerateTokenForUser модуля AuthenticationModule и вернул подписанный токен, когда пользователь действителен. (См., еще часть приведенных ниже фрагментов)

[HttpPost]
        public HttpResponseMessage LoginDemo(string userName, string password)
        {

            MockAuthenticationService demoService = new MockAuthenticationService();
            UserProfile user = demoService.GetUser(userName, password);
            if (user == null)
            {
                return Request.CreateResponse(HttpStatusCode.Unauthorized, "Invalid User", Configuration.Formatters.JsonFormatter);
            }else
            {

                AuthenticationModule authentication = new AuthenticationModule();
                string token = authentication.GenerateTokenForUser(user.UserName, user.UserId);
                return Request.CreateResponse(HttpStatusCode.OK, token, Configuration.Formatters.JsonFormatter);
            }          

        }

Я сделал запрос через Postman (расширение Chrome). Возвращенный токен показан на скриншоте-

В случае недопустимого пользователя результат отображается как

Возвращенный токен передается в качестве заголовка в каждом запросе, а фактическая проверка выполняется с помощью метода IsUserAuthorized в нашем пользовательском фильтре.

public bool IsUserAuthorized(HttpActionContext actionContext)
        {
            var authHeader = FetchFromHeader(actionContext); fetch authorization token from header
           
            if (authHeader != null)
            {
                var auth = new AuthenticationModule();
                JwtSecurityToken userPayloadToken = auth.GenerateUserClaimFromJWT(authHeader);

                if (userPayloadToken != null)
                {
                
                    var identity = auth.PopulateUserIdentity(userPayloadToken);
                    string[] roles = { "All" };
                    var genericPrincipal = new GenericPrincipal(identity, roles);
                    Thread.CurrentPrincipal = genericPrincipal; 
                    var authenticationIdentity = Thread.CurrentPrincipal.Identity as JWTAuthenticationIdentity;
                    if (authenticationIdentity != null && !String.IsNullOrEmpty(authenticationIdentity.UserName))
                    {
                        authenticationIdentity.UserId = identity.UserId;
                        authenticationIdentity.UserName = identity.UserName;
                    }               
                    return true;
                }

            }
             return false;

          
        }

Простая реализация для получения заголовка авторизации.

private string FetchFromHeader(HttpActionContext actionContext)
        {
            string requestToken = null;

            var authRequest = actionContext.Request.Headers.Authorization;
            if (authRequest != null)
            {
                requestToken = authRequest.Parameter;
            }

            return requestToken;
        }

Метод IsUserAuthorized теперь говорит сам за себя. Выполняемые шаги вызывают второй и третий метод AuthenticationModule (как объяснялось ранее) для заполнения объекта JWTAuthenticationIdentity, который представляет личность текущего пользователя, создавая GenericPrincipalи присваивая его текущему принципу потока запросов. Таким образом, вся информация, относящаяся к пользователю, устанавливается здесь. Эти пользовательские данные можно получить из объекта идентификации текущего запроса. Установка текущего принципа через объект идентификации вдохновлена ​​​​другим подобным блогом на аутентификации на основе токена (без JWT). Для получения более подробной информации вы также можете обратиться к этому блогу.

Попробуем с неверным токеном в заголовке. Метод ShowAuthenticationError, который вызывается через метод OnAuthorize, описан ниже:

private static void ShowAuthenticationError(HttpActionContext filterContext)
        {
            var responseDTO = new ResponseDTO() { Code = 401, Message = "Unable to access, Please login again" };
            filterContext.Response =
            filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized, responseDTO);
        }

Вывод в Postman:

Теперь, если я передам правильный сгенерированный токен, результат будет показан ниже. Он правильно получает список книг, который ожидался в соответствии с действием, определенным в нашем контроллере. Взгляни-

[JWTAuthenticationFilter]
   public class DashboardController : BaseController
    {

        public HttpResponseMessage Get()
        {

            var listOfbooks = GetAllBooks();

            return Request.CreateResponse(HttpStatusCode.OK, listOfbooks);
        }

        private List<Books> GetAllBooks()
        {
            List<Books> book = new List<Books>();
            book.Add(new Books { Id = 1, Name = "ABC Books" });
            book.Add(new Books { Id = 2, Name = "XYZ Books" });
            book.Add(new Books { Id = 3, Name = "DEF Books" });
            return book;
        }

    }

Обратите внимание на контроллер, украшенный атрибутами JWTAuthenticationFilter.

Заключительные пункты

Подход на основе JWT для безопасной передачи данных использует возможности цифровой подписи и хеширования. Лучшее использование, которое я нашел, - это аутентификация/авторизация. Он хорошо сочетается с протоколом без сохранения состояния, таким как службы REST, и поэтому особенно полезен для создания служб API для разработки мобильных приложений. В большинстве реализаций он также экономит поиск в базе данных, сохраняя часть информации, такую ​​​​как роли, UserId в утверждениях. Нет нужды упоминать об экономии места в памяти по сравнению с традиционным подходом.
Поскольку MVC и веб-API позволяют внедрить нашу логику авторизации через настраиваемые фильтры, JWT можно легко внедрить. Это была очень простая реализация, и я с нетерпением жду предложений и лучших вариантов использования для реализации.

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

Спасибо, что нашли время, чтобы прочитать мой блог.

Этот блог создан Анджани Сингхом и первоначально опубликован на сайте blogs.quovantis.com 25 сентября 2017 г.