Что такое сервер авторизации
Перейти к содержимому

Что такое сервер авторизации

  • автор:

OAuth 2: введение в протокол авторизации

Эта инструкция — часть курса «Как работают сетевые протоколы».

Смотреть весь курс

Изображение записи

В статье рассмотрим механику, способы и примеры использования технологии.

Что такое OAuth 2 — обзор

OAuth 2 — это протокол авторизации, предназначенный для организации доступа клиентских приложений к ресурсам, или данным учетных записей, пользователя на другом сервисе. В качестве клиентских приложений выступают веб-сервисы, мобильные и десктопные приложения. В качестве сервисов — mail.ru, GitHub, Bitbucket и др. Протокол используют разработчики сторонних приложений.

Мы сталкиваемся с этим протоколом, когда:

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

Доступ может быть ограничен правами пользователя или же областями видимости, что повышает гибкость использования протокола. Например, стороннее приложение может только читать наши данные, а не изменять их, либо же только изменять.

Различия протоколов OpenID и OAuth 2

Основное различие между этими двумя протоколами состоит в цели использования. Так, OpenID служит для аутентификации, то есть подтверждения личности пользователя в клиентском сервисе. Например, вы можете авторизоваться в любом из сервисов Google под своим аккаунтом и работать с ними от своего имени, со своими данными. OAuth же представляет собой протокол авторизации, то есть выдачи клиентскому сервису прав на выполнение действий с ресурсами пользователя (как правило, на чтение данных, но иногда и на изменение) от его имени.

Для верификации пользователя OpenID использует ID учетной записи у провайдера, а OAuth — авторизационные ключи (токены) с предопределенным сроком действия и правами доступа.

Используемые роли в OAuth 2

В рамках описываемого протокола выделяются следующие типы ролей:

  • владелец (пользователь): авторизует клиентское приложение на доступ к данным своего аккаунта;
  • сервер ресурсов/API: здесь располагаются данные пользовательских аккаунтов, а также бизнес-логика авторизации, отвечающая за выдачу новых OAuth-токенов и проверку их подлинности при обращении клиентских приложений к ресурсам. Целесообразно объединять эти роли, так как физически это один сервис;
  • клиентское приложение: собственно сервис, которому пользователь делегирует права доступа к своим данным на сервере ресурсов. Пользователь должен авторизовать приложение, а со стороны сервера API оно должно получить подтверждение в виде ключа (токена) доступа.

Как работает OAuth 2: от запроса на авторизацию до генерации токена

Рассмотрим схему, описывающую принцип действия протокола.

принцип действия протокола

Выделим следующие шаги:

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

В зависимости от бизнес-логики клиентского приложения последовательность шагов может меняться. Далее рассмотрим наиболее распространенные примеры использования OAuth 2.

Регистрация приложения на сервере API

Вне зависимости от того, с каким именно сервисом выполняется интеграция вашего приложения, его необходимо в первую очередь зарегистрировать. Делается это с помощью специального портала на сайте сервиса, с которым выполняется интеграция (например, https://console.cloud.google.com для Google).

Заполните все необходимые сведения о приложении:

  • тип,
  • используемые сервисы,
  • название,
  • информация о разработчике и пр.

Также в зависимости от архитектуры приложения, возможно, потребуется предоставить callback или redirect url — адрес, на который ваше приложение будет перенаправлять пользователя после успешной авторизации или же отказа от нее.

На финальном этапе вам будут предоставлены два строковых ключа: client_id (ID клиента) и client_secret (секрет клиента). Первый служит для идентификации приложения, а также генерации авторизационных URL для пользователей — параметр является публичным. Секрет же предназначен для проверки подлинности приложения API сервисом в тот момент, когда оно запрашивает доступ к пользовательскому аккаунту. Секрет должен быть известен только приложению и API.

OAuth для приложений с серверной частью: рассмотрим по шагам

Последовательность шагов приведена на схеме ниже.

действие для приложений с серверной частью

  1. Пользователь перенаправляется на страницу авторизации, где у него запрашиваются разрешения для приложения на работу с данными его аккаунта.
  2. После предоставления необходимых разрешений пользователь попадает на callback URL — адрес, указанный при регистрации приложения, предназначенный для завершения авторизации. При этом происходит подстановка кода авторизации в GET-параметры адреса.
  3. Сервер клиентского приложения формирует POST-запрос к серверу авторизации API с кодом авторизации в качестве параметра.
  4. Сервер авторизации проверяет код и возвращает приложению токен доступа (access token).
  5. Используя токен, приложение авторизуется на сервере API и получает доступ к запрашиваемым пользовательским ресурсам.

Стоит отметить, что описываемый вариант авторизации является самым сложным, но только в рамках этой механики можно достоверно идентифицировать клиентское приложение (благодаря коммуникации между серверами на последнем шаге). Во всех остальных случаях авторизация проходит только на клиенте, что позволяет злоумышленникам маскировать одно приложение под другое. Данный фактор необходимо учитывать при внедрении механизма OAuth авторизации в своих сервисах.

Пример реализации использования протокола

У нас есть приложение с серверной частью, использующее API mail.ru.

Перенаправим пользователя на страницу авторизации.

> GET /oauth/authorize?response_type=code&client_id=464119& redirect_uri=http%3A%2F%2Fexample.com%2Fcb%2F123 HTTP/1.1 > Host: connect.mail.ru

Значения параметров ID клиента (client_id), секрета клиента (client_secret) и URL страницы подтверждения авторизации (redirect_uri) разработчик получает при регистрации своего приложения на платформе.

Когда пользователь предоставит приложению разрешение на доступ к своему аккаунту, он будет перенаправлен на redirect_uri:

Рекомендуем в redirect_uri добавлять уникальный идентификатор пользователя для предотвращения CSRF-атак (в приведенном примере это 123). При получении кода авторизации необходимо проверить подлинность идентификатора и его соответствие текущему пользователю.

Далее необходимо обменять код авторизации на ключ доступа (токен):

> POST /oauth/token HTTP/1.1 > Host: connect.mail.ru > Content-Type: application/x-www-form-urlencoded > > grant_type=authorization_code&client_id=464119&client_secret=deadbeef&code=DoRieb0y& redirect_uri=http%3A%2F%2Fexample.com%2Fcb%2F123 < HTTP/1.1 200 OK < Content-Type: application/json

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

В результате сервер API возвращает токен доступа (access_token), его тип (token_type), срок жизни (expires_in), а также ключ для обновления токена (refresh_token). Полученные данные можно использовать для работы с пользовательским аккаунтом через API:

> GET /platform/api?oauth_token=SlAV32hkKG&client_id=464119&format=json&method=users.getInfo& sig=. HTTP/1.1 > Host: appsmail.ru

OAuth для полностью клиентских приложений

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

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

принцип действия для полностью клиентских приложений

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

При таком подходе опускается обмен кода авторизации на токен доступа во время обмена запросами между серверами приложения и API. Вся авторизация, как было отмечено ранее, происходит на стороне клиента.

Пример авторизации для приложения только с клиентской частью

У нас есть приложение только с клиентской частью, взаимодействующее по API с сервисами Mail.Ru. Рассмотрим процесс авторизации.

Сначала выполним переход на страницу авторизации:

> GET /oauth/authorize?response_type=token&client_id=464119 HTTP/1.1 > Host: connect.mail.ru

После подтверждения делегирования прав произойдет редирект на страницу-заглушку:

Авторизуемое приложение должно зафиксировать последний редирект и изъять из параметров URL необходимые данные для доступа к сервису по API.

Авторизация по логину и паролю

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

Пример: имеется некоторое стороннее приложение, работающее с сервисами Mail.Ru по API. При этом в нем применена базовая авторизация.

Отправим запрос с нашими учетными данными и получим данные для вызова API:

> POST /oauth/token HTTP/1.1 > Host: connect.mail.ru > Content-Type: application/x-www-form-urlencoded > > grant_type=password&client_id=31337&client_secret=deadbeef&username=api@corp.mail.ru& password=qwerty < HTTP/1.1 200 OK < Content-Type: application/json

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

Пример: https://oauth.example.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET.

Обновление доступа

Как можно было видеть в предыдущих примерах, вместе с токеном при успешной авторизации возвращается также и ключ для обновления доступа (refresh_token). Он может использоваться для обновления токена по окончании его срока жизни, а также принудительного обновления во избежание компрометации токена при его передаче по открытым каналам. В случае успешного обновления доступа сервер API перевыпустит не только access, но и refresh_token.

> POST /oauth/token HTTP/1.1 > Host: connect.mail.ru > Content-Type: application/x-www-form-urlencoded > > grant_type=refresh_token&client_id=31337&client_secret=deadbeef&refresh_token=8xLOxBtZp8 < HTTP/1.1 200 OK < Content-Type: application/json

Преимущества и недостатки OAuth 2

Рассмотрим подробнее плюсы и минусы протокола авторизации.

Плюсы

  • использование SSL для защиты данных,
  • ограничение прав доступа стороннего приложения к пользовательским ресурсам областями видимости,
  • обилие библиотек для реализации использования протокола во всех языках общего назначения.

Минусы

  • различия в подходах к реализации у разных сервисов, порождающие необходимость написания отдельного кода под каждый новый сервис;
  • если реализована интеграция с семейством приложений, работающих в одной экосистеме (например, Google), существует риск для всех интеграций при компрометации учетных данных либо сбое на стороне сервера API;
  • OAuth 2.0 является новым и динамично развивающимся протоколом, в связи с чем возможны некоторые логические противоречия в его спецификации;
  • основанная на SSL безопасность протокола может стать проблемой в высоконагруженных проектах.

Пример реализации протокола на PHP

Рассмотрим живой пример проекта клиент-серверного стороннего приложения, взаимодействующего по API с сервисом Google Таблицы.

После регистрации приложения на console.developers.google.com получим файл учетных данных и сохраним его на диске сервера:

получим файл учетных данных

Также сохраним файл с информацией о проекте и авторизационными данными:

сохраним файл с информацией о проекте

Установим библиотеку GoogleApiClient для PHP и подготовим код:

Установим библиотеку GoogleApiClient

Установим библиотеку GoogleApiClient и подготовим код

установка библиотеки и подготовка кода

Код приведенного класса устанавливает соединение с сервисом API Google Таблиц, используя учетные данные в файле (credentials.json), и обменивает их на токен авторизации, с помощью которого выполняет обращение к содержимому конкретной таблицы (аргументы $sheetRange и $spreadSheetId). При этом реализована проверка жизнеспособности токена для последующих вызовов (метод checkTokenExpired). Полученный токен записывается в файл token.json и доступен для последующих вызовов API через библиотеку:

токен записывается в файл token.json

Далее используем полученный класс для обращения к данным внутри кода проекта:

обращение к данным внутри кода проекта

Здесь устанавливается подключение к таблицам выгрузок из CRM и расходов, а затем полученные данные обрабатываются и используются далее .

Таким образом, OAuth 2 предоставляет гибкие возможности для быстрой интеграции с внешними сервисами.

Заключение

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

Автор текста: Роман Андреев.

Принципы работы протокола DHCP

Сервер авторизации для микросервисов на Spring Boot

В этой статье рассмотрим, как реализовать аутентификацию с помощью централизованного сервера авторизации (Authorization Server) и API-шлюза (API Gateway).

Что такое JWT-токен и зачем его использовать?

JSON Web Token (JWT) — это стандарт передачи информации с полезной нагрузкой в формате JSON в виде некоторого количества утверждений (claim) с опциональной подписью и/или шифрованием.

JWT-токены содержат сведения для аутентификации и могут использоваться в нескольких сервисах, инстансах для реализации stateless-аутентификации (без сохранения состояния). При использовании JWT-токенов нет необходимости содержать отдельные ресурсы для пользовательских сессий или хранить токены/сессии в отдельной базе данных/кэше.

Архитектура решения

Отметим основные моменты:

  • Для создания и проверки JWT-токенов используется централизованный сервер авторизации (Authorization Server).
  • API Gateway представляет собой единую точку входа в приложение, которая перенаправляет запросы к соответствующим микросервисам.
  • К маршрутам (routes) добавляется Gateway-фильтр, проверяющий JWT-токены в запросах к защищаемым ресурсам. Для валидации токена и получения имени пользователя с его полномочиями выполняется обращение к серверу авторизации (Authorization Server). Далее полученная информация передается другим сервисам в заголовке запроса.
  • Для Service Discovery (обнаружения сервисов) будем использовать Eureka Discovery Client.

Аутентификация

Аутентификация происходит следующим образом:

  • Пользователь логинится (создает токен аутентификации), вызывая конечную точку /login (POST) с передачей имени пользователя и пароля. В ответ в заголовке он получает Bearer-токен.
  • Токен передается в заголовке в параметре Authorization в формате Bearer access_token .
  • Для запросов к защищенным ресурсам вызывается кастомный Gateway Filter (AuthenticationPrefilter). В фильтре выполняется обращение к конечной точке /api/v1/validateToken сервиса аутентификации (Authentication Service), который валидирует токен и, в случае успешной проверки, отправляет в ответ имя пользователя и его полномочия (authorities).
  • Если токен валидный, то перед переадресацией на ресурс, запрошенный пользователем, к заголовку запроса добавляются имя пользователя и полномочия.
  • В остальных микросервисах (например, user-service) фильтр авторизации, наследуемый от OncePerRequestFilter , создает объект Authentication , используя класс UsernamePasswordAuthenticationToken (с username и SimpleGrantedAuthority из заголовка, с паролем null).
  • Если у пользователя есть полномочия/доступ к ресурсу, то запрос разрешается. В противном случае клиенту возвращается ответ 401 Unathorized / 403 Forbidden.

Пишем сервисы

Eureka Server

  • Создайте приложение Spring Boot, через Spring Initializr с зависимостью spring-cloud-starter-netflix-eureka-server . Также добавьте spring-cloud-dependencies в dependencyManagement .
  • Теперь для запуска Eureka Server достаточно добавить аннотацию @EnableEurekaServer к основному классу приложения.
  • В property-файл добавьте следующие настройки Eureka Server:
spring.application.name=naming-server server.port=8761 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false eureka.instance.prefer-ip-address=true

  • Eureka Server будет доступен по адресу http://localhost:8761/. На главной странице можно увидеть список зарегистрированных сервисов.

Authorization Service (сервис авторизации)

  • Создайте приложения Spring Boot со следующими зависимостями: spring-boot-starter-security , spring-boot-starter-web , spring-cloud-starter-sleuth , spring-cloud-starter-config , spring-cloud-starter-netflix-eureka-client , spring-boot-starter-data-jpa , spring-boot-starter-data-mongodb , spring-boot-starter-data-redis и lombok .
  • Зависимость spring-boot-starter-security необходима для авторизации и аутентификации, spring-boot-starter-data-mongodb и spring-boot-starter-data-jpa для доступа к учетным данным в MongoDB. Для создания и проверки JWT-токенов будем использовать io.jsonwebtoken:jjwt .
  • Для аутентификации с использованием учетных данных в базе данных напишем свою реализацию UserDetailsService из Spring Security. Для получения учетных данных пользователя из базы данныхи создания экземпляра UserDetails необходимо реализовать метод loadUserByUsername() .
@Service public class ApplicationUserDetailsService implements UserDetailsService < @Autowired private UsersService usersService; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException < return new ApplicationUsers(usersService.getByUsrName(s).orElseThrow(() ->new UsernameNotFoundException("Username Not Found"))); > >
  • Своя реализация UserDetails нам нужна для маппинга объектов, хранящихся в базе данных, на объекты, требуемые Spring Security.
  • Создаем класс конфигурации — наследник WebSecurityConfigurerAdapter .
package com.infotrends.in.authenticationserver.security.config; import com.infotrends.in.authenticationserver.security.filters.JWTAuthenticationFilter; import com.infotrends.in.authenticationserver.security.filters.JWTVerifierFilter; import com.infotrends.in.authenticationserver.security.services.ApplicationUserDetailsService; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter < @Autowired private PasswordEncoder encoder; @Autowired private ApplicationUserDetailsService applicationUserDetailsService; @Autowired private TokensRedisService redisService; @Override protected void configure(HttpSecurity http) throws Exception < http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilter(new JWTAuthenticationFilter(authenticationManager(), redisService)) .addFilterAfter(new JWTVerifierFilter(redisService), JWTAuthenticationFilter.class) .authorizeRequests() .antMatchers("/api/v1/validateConnection/whitelisted").permitAll() .anyRequest() .authenticated() .and().httpBasic(); >@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception < auth.authenticationProvider(authenticationProvider()); >@Bean public DaoAuthenticationProvider authenticationProvider() < DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setPasswordEncoder(encoder); authenticationProvider.setUserDetailsService(applicationUserDetailsService); return authenticationProvider; >>
  • Здесь мы создаем бин DaoAuthenticationProvider с кодировщиком паролей и нашей реализацией UserDetailsService .
  • Которые, в свою очередь, используются для переопределения configure(AuthenticationManagerBuilder auth) , настраивающего AuthenticationManagerBuilder для использования созданного нами Authentication Provider.
  • Далее для использования JWT/Bearer-токенов вместо аутентификации по логину и паролю, надо настроить два фильтра: один для генерации Bearer-токена, а другой — для его проверки.
  • Фильтр генерации JWT создаем как подкласс UsernamePasswordAuthenticationFilter . Для проверки учетных данных переопределяем метод attemptAuthentication() . Для создания JWT-токена при успешной аутентификации — метод и successAuthentication ().
package com.infotrends.in.authenticationserver.security.filters; import com.fasterxml.jackson.databind.ObjectMapper; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.authenticationserver.model.ConnValidationResponse; import com.infotrends.in.authenticationserver.model.JwtAuthenticationModel; import com.infotrends.in.authenticationserver.model.redis.TokensEntity; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import com.infotrends.in.authenticationserver.utils.Utilities; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Date; @Slf4j @RequiredArgsConstructor public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter < private final AuthenticationManager authenticationManager; private ObjectMapper mapper=new ObjectMapper(); private final TokensRedisService tokensRedisService; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException < try < JwtAuthenticationModel authModel = mapper.readValue(request.getInputStream(), JwtAuthenticationModel.class); Authentication authentication = new UsernamePasswordAuthenticationToken(authModel.getUsername(), authModel.getPassword()); return authenticationManager.authenticate(authentication); >catch (IOException e) < throw new RuntimeException(e); >> @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException < String token = Jwts.builder() .setSubject(authResult.getName()) .claim("authorities", authResult.getAuthorities()) .claim("principal", authResult.getPrincipal()) .setIssuedAt(new Date()) .setIssuer(SecurityConstants.ISSUER) .setExpiration(Date.from(LocalDateTime.now().plusMinutes(30).toInstant(ZoneOffset.UTC))) .signWith(SignatureAlgorithm.HS256, SecurityConstants.KEY) .compact(); log.info(token); TokensEntity tokensEntity = TokensEntity.builder().id(Utilities.generateUuid()).authenticationToken(token) .username(authResult.getName()) .createdBy("SYSTEM").createdOn(LocalDateTime.now()) .modifiedBy("SYSTEM").modifiedOn(LocalDateTime.now()) .build(); tokensEntity = tokensRedisService.save(tokensEntity); response.addHeader(SecurityConstants.HEADER, String.format("Bearer %s", tokensEntity.getId())); // response.addHeader("Expiration", String.valueOf(30*60)); ConnValidationResponse respModel = ConnValidationResponse.builder().isAuthenticated(true).build(); response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); response.getOutputStream().write(mapper.writeValueAsBytes(respModel)); >>
  • Фильтр проверки JWT наследуем от OncePerRequestFilter и настраиваем его вызов после фильтра, генерирующего JWT, с помощью addFilterAfter() в классе конфигурации WebSecurityConfig .
package com.infotrends.in.authenticationserver.security.filters; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.authenticationserver.model.redis.TokensEntity; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import com.infotrends.in.authenticationserver.utils.Utilities; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import org.apache.tomcat.util.http.parser.Authorization; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @RequiredArgsConstructor public class JWTVerifierFilter extends OncePerRequestFilter < private final TokensRedisService tokensRedisService; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException < String bearerToken = httpServletRequest.getHeader(SecurityConstants.HEADER); if(!(Utilities.validString(bearerToken) && bearerToken.startsWith(SecurityConstants.PREFIX))) < filterChain.doFilter(httpServletRequest, httpServletResponse); return; >String authToken = bearerToken.replace(SecurityConstants.PREFIX, ""); Optional tokensEntity = tokensRedisService.findById(authToken); if(!tokensEntity.isPresent()) < filterChain.doFilter(httpServletRequest, httpServletResponse); return; >String token = tokensEntity.get().getAuthenticationToken(); Jws authClaim = Jwts.parser().setSigningKey(SecurityConstants.KEY) .requireIssuer(SecurityConstants.ISSUER) .parseClaimsJws(token); String username = authClaim.getBody().getSubject(); List> authorities = (List>) authClaim.getBody().get("authorities"); List grantedAuthorities = authorities.stream().map(map -> new SimpleGrantedAuthority(map.get("authority"))) .collect(Collectors.toList()); Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authentication); httpServletRequest.setAttribute("username", username); httpServletRequest.setAttribute("authorities", grantedAuthorities); filterChain.doFilter(httpServletRequest, httpServletResponse); > >
package com.infotrends.in.authenticationserver.resources; import com.sun.security.auth.UserPrincipal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.GrantedAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @RequestMapping("/api/v1/validateToken") public class ConnectionValidatorResource < @GetMapping(value = "", produces = ) public ResponseEntity validateGet(HttpServletRequest request) < String username = (String) request.getAttribute("username"); ListgrantedAuthorities = (List) request.getAttribute("authorities"); return ResponseEntity.ok(ConnValidationResponse.builder().status("OK").methodType(HttpMethod.GET.name()) .username(username).authorities(grantedAuthorities) .isAuthenticated(true).build()); > @Getter @Builder @ToString public class ConnValidationResponse < private String status; private boolean isAuthenticated; private String methodType; private String username; private Listauthorities; > >

Дополнительная функциональность

Как правило, содержимое JWT-токена нельзя изменить после его создания. Однако токен можно легко расшифровать и прочитать.

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

Таким образом, клиентскому приложению будет возвращен только сгенерированный UUID, а JWT-токен будет использоваться другими сервисами для авторизации/аутентификации.

package com.infotrends.in.authenticationserver.model.redis; import lombok.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisHash; import java.time.LocalDateTime; @RedisHash(value = "Tokens", timeToLive = 86400) @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class TokensEntity

API Gateway

  • Создайте приложение Spring Boot со следующими зависимостями, необходимыми для API Gateway с Eureka Client: spring-cloud-starter-gateway , spring-cloud-starter-config и spring-cloud-starter-netflix-eureka-client .
  • Файл настроек Cloud Config Server и Eureka Server:
debug: true logging: level: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG server: port: '8765' spring: cloud: config: profile: dev gateway: discovery.locator.enabled: true config: import: optional:configserver:http://clouduser:configserver705!@localhost:8888 application: name: api-gateway jackson: date-format: yyyy-MM-dd HH:mm:ss management: endpoints: web: exposure: include: '*' eureka: client: serviceUrl: defaultZone: http://eurekauser:eureka124!@localhost:8761/eureka instance: prefer-ip-address: 'true'
  • Далее добавляем к классу ApiGatewayApplication аннотацию @EnableFeignClients для написания запросов к Eureka Server.
  • И настраиваем Gateway Filter, который проверяет Bearer-токен в запросах, используя конечную точку /validateToken на сервере авторизации. Для этого наследуемся от класса AbstractGatewayFilterFactory , предоставленного Spring-API Gateway, и переопределяем метод apply(Config config) , который возвращает реализацию GatewayFilter .
package com.infotrends.in.InfoTrendsIn.ApiGateway.filters; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.infotrends.in.InfoTrendsIn.ApiGateway.model.Authorities; import com.infotrends.in.InfoTrendsIn.ApiGateway.model.ConnValidationResponse; import com.infotrends.in.InfoTrendsIn.ApiGateway.utils.Utilities; import com.infotrends.in.InfoTrendsIn.exceptions.model.ExceptionResponseModel; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Date; import java.util.List; import java.util.function.Predicate; @Component @Slf4j public class AuthenticationPrefilter extends AbstractGatewayFilterFactory  < @Autowired @Qualifier("excludedUrls") ListexcludedUrls; private final WebClient.Builder webClientBuilder; public AuthenticationPrefilter(WebClient.Builder webClientBuilder) < super(Config.class); this.webClientBuilder=webClientBuilder; >@Autowired private ObjectMapper objectMapper; @Override public GatewayFilter apply(Config config) < return (exchange, chain) -> < ServerHttpRequest request = exchange.getRequest(); log.info("**************************************************************************"); log.info("URL is - " + request.getURI().getPath()); String bearerToken = request.getHeaders().getFirst(SecurityConstants.HEADER); log.info("Bearer Token: "+ bearerToken); if(isSecured.test(request)) < return webClientBuilder.build().get() .uri("lb://authentication-service/api/v1/validateToken") .header(SecurityConstants.HEADER, bearerToken) .retrieve().bodyToMono(ConnValidationResponse.class) .map(response -> < exchange.getRequest().mutate().header("username", response.getUsername()); exchange.getRequest().mutate().header("authorities", response.getAuthorities().stream().map(Authorities::getAuthority).reduce("", (a, b) ->a + "," + b)); return exchange; >).flatMap(chain::filter).onErrorResume(error -> < log.info("Error Happened"); HttpStatus errorCode = null; String errorMsg = ""; if (error instanceof WebClientResponseException) < WebClientResponseException webCLientException = (WebClientResponseException) error; errorCode = webCLientException.getStatusCode(); errorMsg = webCLientException.getStatusText(); >else < errorCode = HttpStatus.BAD_GATEWAY; errorMsg = HttpStatus.BAD_GATEWAY.getReasonPhrase(); >// AuthorizationFilter.AUTH_FAILED_CODE return onError(exchange, String.valueOf(errorCode.value()) ,errorMsg, "JWT Authentication Failed", errorCode); >); > return chain.filter(exchange); >; > public Predicate isSecured = request -> excludedUrls.stream().noneMatch(uri -> request.getURI().getPath().contains(uri)); private Mono onError(ServerWebExchange exchange, String errCode, String err, String errDetails, HttpStatus httpStatus) < DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory(); // ObjectMapper objMapper = new ObjectMapper(); ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(httpStatus); try < response.getHeaders().add("Content-Type", "application/json"); ExceptionResponseModel data = new ExceptionResponseModel(errCode, err, errDetails, null, new Date()); byte[] byteData = objectMapper.writeValueAsBytes(data); return response.writeWith(Mono.just(byteData).map(t ->dataBufferFactory.wrap(t))); > catch (JsonProcessingException e) < e.printStackTrace(); >return response.setComplete(); > @NoArgsConstructor public static class Config < >>

Маршруты

В отдельной конфигурации настраиваем маршруты (routes) на использование созданного выше GatewayFilter .

package com.infotrends.in.InfoTrendsIn.ApiGateway.config; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.infotrends.in.InfoTrendsIn.ApiGateway.filters.AuthenticationPrefilter; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.text.SimpleDateFormat; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Configuration public class RouteConfiguration < @Bean public RouteLocator routes( RouteLocatorBuilder builder, AuthenticationPrefilter authFilter) < return builder.routes() .route("auth-service-route", r ->r.path("/authentication-service/**") .filters(f -> f.rewritePath("/authentication-service(?/?.*)", "$\\") .filter(authFilter.apply( new AuthenticationPrefilter.Config()))) .uri("lb://authentication-service")) .route("user-service-route", r -> r.path("/user-service/**") .filters(f -> f.rewritePath("/user-service(?/?.*)", "$\\") .filter(authFilter.apply( new AuthenticationPrefilter.Config()))) .uri("lb://user-service")) .build(); > >

User-Service

  • Создайте проект User-Service с помощью Spring Initializr. В этом сервисе будут храниться пользователи. Это будет пример бэкенда.
  • Создайте класс конфигурации, наследуя класс WebSecurityConfigurerAdapter , и переопределите метод void configure(HttpSecurity http) . Здесь мы настроим запуск нашего фильтра проверки токена (JWTVerifierFilter) перед UsernamePasswordAuthenticationFilter .
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter< @Autowired private PasswordEncoder encoder; @Value("$") private String username; @Value("$") private String password; @Autowired private AppUserDetailsService appUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception < http.csrf().disable() .headers().frameOptions().disable() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(new JWTVerifierFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers(HttpMethod.GET, "/api/v1/users").permitAll() .anyRequest() .authenticated() .and().httpBasic(); >>
  • JWTVerifierFilter проверяет наличие в заголовке запроса данных об имени пользователя и полномочиях, и создает объект Authentication, используя класс UsernamePasswordAuthenticationToken .
  • Далее, используя SecurityContextHolder , сохраняет authentication в контекст безопасности Spring Security.
package com.infotrends.in.InfoTrendsIn.config.security.filters; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.InfoTrendsIn.utils.Utilities; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; public class JWTVerifierFilter extends OncePerRequestFilter < @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException < String authHeader = httpServletRequest.getHeader("Authorization"); if(!Utilities.validString(authHeader) || !authHeader.startsWith("Bearer ")) < filterChain.doFilter(httpServletRequest, httpServletResponse); return; >logHeaders(httpServletRequest); String username=httpServletRequest.getHeader("username"); List> authorities = new ArrayList<>(); String authoritiesStr = httpServletRequest.getHeader("authorities"); Set simpleGrantedAuthorities = new HashSet<>(); if(Utilities.validString(authoritiesStr)) < simpleGrantedAuthorities=Arrays.stream(authoritiesStr.split(",")).distinct() .filter(Utilities::validString).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());; >Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(httpServletRequest, httpServletResponse); > private void logHeaders(HttpServletRequest httpServletRequest) < EnumerationheaderNames = httpServletRequest.getHeaderNames(); while(headerNames.hasMoreElements()) < String header=headerNames.nextElement(); logger.info(String.format("Header: %s --- Value: %s", header, httpServletRequest.getHeader(header))); >> >

Таким образом, запрос к ресурсу / конечной точке разрешается, если запрашивающий их пользователь имеет соответствующий доступ/полномочия (authority). Ниже приведен пример для GET-запроса.

@PreAuthorize("hasAnyAuthority('USER_READ', 'USER')") @GetMapping(value = "/", produces = ) public ResponseEntity> getUserById(@PathVariable("id") String id) < UsersResponseModel respModel = new UsersResponseModel(); Optionaluser = usersSvc.findById(id); if(!user.isPresent()) < throw new UserExceptions.UserNotFoudException(ErrorsMappings.USER_NOT_FOUND_MESSAGE); >respModel.setUser(user.get()); respModel.setCode(HttpStatus.OK.value()); EntityModel entity = EntityModel.of(respModel); entity = usersProcess.generateHateoas(entity, this, "view-user", user.get().getId()); return new ResponseEntity(entity, HttpStatus.OK); >

Используемые зависимости:

 org.springframework.boot spring-boot-starter-security  org.projectlombok lombok true  io.jsonwebtoken jjwt 0.9.1  org.springframework.cloud spring-cloud-starter-config  org.springframework.cloud spring-cloud-starter-netflix-eureka-client 

Для использования Config Server и Eureka Client также добавим следующие зависимости в раздел dependencyManagement.

 2020.0.3    org.springframework.cloud spring-cloud-dependencies $ pom import   

Использование JWT-токена

Проверка запросов к микросервисам

  • API для аутентификации пользователя и генерации JWT-токена:

  • API validateToken, используемый для валидации токена, отправляемого в запросе, и получения сведений об авторизации. (Этот API позже будет заблокирован от внешнего доступа).

  • Запрос к защищенному ресурсу в User-Service при вызове через API Gateway с валидным токеном авторизации:

  • Запрос к защищенному ресурсу в User-Service при вызове через API Gateway без валидного токена:

  • Пример JWT-токена, созданный сервисом авторизации:

Сегодня в 20:00 состоится открытое занятие «Структура программы на Java. Примитивные типы». На этом открытом уроке вы сможете познакомиться с основными этапами создания простейшей программы на Java, понять принципы работы компилятора и виртуальной машины, а также разобраться с class-файлами. На уроке мы изучим примитивные типы данных, константы и enum. Регистрация доступна по ссылке для всех желающих.

6.3. Серверы авторизации

Серверы авторизации - это внешние источники учетных записей пользователей, например, LDAP-сервер, или серверы, производящие аутентификацию для UserGate, например, Radius, TACACS+, Kerberos, SAML. Система поддерживает следующие типы серверов авторизации:

  • LDAP-коннектор
  • Сервер авторизации пользователей Radius
  • Сервер авторизации пользователей TACACS+
  • Сервер авторизации Kerberos
  • Сервер авторизации NTLM
  • Сервер авторизации SAML (SSO)

Серверы авторизации Radius, TACACS+, NTLM, SAML могут осуществлять только авторизацию пользователей, в то время как LDAP-коннектор позволяет также получать информацию о пользователях и их свойствах.

В разделе Серверы авторизации находится кнопка Сбросить авторизацию для всех пользователей, которая стирает информацию в UserGate обо всех идентифицированных пользователях. После нажатия этой кнопки всем пользователям необходимо будет пройти авторизацию заново.

  • 6.3.1. LDAP-коннектор
  • 6.3.2. Сервер авторизации пользователей Radius
  • 6.3.3. Сервер авторизации пользователей TACACS+
  • 6.3.4. Сервер авторизации пользователей SAML IDP
  • 6.3.5. Сервер авторизации NTLM
  • 6.3.6. Метод авторизации Kerberos
  • 6.3.7. Метод авторизации HTTP Basic

Что такое "сервер авторизации" и зачем он нужен?

Программный комплекс SocialKit во время работы использует наши удалённые сервера для учёта лицензий и контроля за ними. Такие сервера мы называем серверами авторизации. Это наше собственное определение, которое к авторизации Instagram-аккаунтов, аккаунтов ВКонтакте, E-Mail'ов, прокси и прочих личных данных отношения не имеет.

Таким образом, наши сервера не используются программой при авторизации каких бы то ни было аккаунтов и во время последующей работы с ними, а ваши персональные данные (логины и пароли от аккаунтов, E-Mail'ов, прокси и т.п.) в автоматическом режиме мы не получаем, получить никак не можем и, соответственно, не храним. Данные от аккаунтов, E-Mail'ов прокси и т.п., а также другие личные сведения передаются с вашего ПК в момент инициализации аккаунтов, E-Mail'ов, прокси и т.п. только на сервера соответствующих сервисов, как это требуется техническими правилами работы искомых сервисов и не более того. К вопросам конфиденциальности мы подходим достаточно строго.

Единственная возможность получить ваши личные данные в виде логинов и паролей от аккаунтов, E-Mail'ов, прокси и т.п. - это запросить у вас эти данные в приватном тикете, если это, на наш взгляд, может помочь в решении той проблемы, с которой вы обратились в искомом тикете. Даже в этом случае мы гарантируем, что полученные личные данные будут использованы техническими специалистами только в ходе решения искомой проблемы, и будут удалены ответственным техническим специалистом, запросившим их, сразу после завершения тикета.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *