Security микросервисов с помощью Spring Cloud Gateway и TokenReley

от автора

Данная статья результат поиска некоего каноничного решения организации безопасности доступа к ресурсам в микросервисной архитектуре, построенной в экосистеме Spring. После прочтения десятка статей по данной тематике, к сожалению, не нашел то, что искал. Spring Security оказался одной из самых недопонятых технологий. Основная проблема у всех — изобретение своего велосипеда поверх стандартного функционала Spring Security. Зачастую, данные статьи сопровождаются комментариями никогда так не делать. И у многих, наверно, возникает вопрос, а как собственно можно делать. Ситуацию несколько прояснила официальная документация. Взяв её за основу, я хочу показать, как можно организовать безопасность микросервисов максимально простым и быстрым способом.

Для начала рассмотрим реализуемую схему authorization flow:

Как видно на картинке, дизайн состоит из трех служб: единой точки входящих запросов от пользователей, реализующей Gateway API, IDP сервера (Identity Provider), который аутентифицирует пользователей и выдает токен доступа и сервера ресурсов, который отдает данные. Входящий запрос, не прошедший проверку подлинности, поступает на Gateway и инициирует authorization flow. Gateway делегирует управление учетными записями пользователей и авторизацию IDP серверу. IDP сервер проверяет учетную запись пользователя и возвращает на Gateway токен доступа. Gateway прикрепляет токен к запросу пользователя и отправляет на сервер ресурса. Сервер ресурсов получает от IDP сервера открытый ключ для самостоятельной валидации токена и в случае успешной валидации возвращает запрашиваемые данные. В этой схеме нет ничего необычного, стандартный OAuth 2.0 подход, основная фишка здесь в том, что практически весь этот функционал доступен из коробки и реализуется подключением нужных зависимостей и конфигурированием property, без необходимости писать какой-то сложный код. Далее я приведу пример, как это можно реализовать, создав три соответствующих данной схеме микросервиса. Весь приведенный код доступен на GitHub.

Spring Authorization Server

В качестве Single Sign-On Identity-провайдера я буду использовать Spring Authorization Server, как максимально простой способ поднять сервер авторизации в виде простого Spring Boot приложения, без дополнительных приседаний. При желании, здесь может быть Keyclock или другая сторонняя служба. Для создания сервера нам потребуются следующие зависимости:

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>   <groupId>org.springframework.security</groupId>   <artifactId>spring-security-oauth2-authorization-server</artifactId>   <version>0.3.1</version> </dependency>

В application.yml укажем порт:

server:   port: 9000

Далее мы создадим конфигурацию bean-компонентов Spring специфичных для OAuth.

@Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig {      @Bean     @Order(Ordered.HIGHEST_PRECEDENCE)     public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {         OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);         return http.formLogin(Customizer.withDefaults()).build();     }      @Bean     public RegisteredClientRepository registeredClientRepository() {         RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())                 .clientId("gateway")                 .clientSecret("{noop}secret")                 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)                 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)                 .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)                 .redirectUri("http://127.0.0.1:8080/login/oauth2/code/gateway")                 .scope(OidcScopes.OPENID)                 .scope("resource.read")                 .build();          return new InMemoryRegisteredClientRepository(registeredClient);     }      @Bean     public JWKSource<SecurityContext> jwkSource() {         RSAKey rsaKey = generateRsa();         JWKSet jwkSet = new JWKSet(rsaKey);         return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);     }      private static RSAKey generateRsa() {         KeyPair keyPair = generateRsaKey();         RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();         RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();         return new RSAKey.Builder(publicKey)                 .privateKey(privateKey)                 .keyID(UUID.randomUUID().toString())                 .build();     }      private static KeyPair generateRsaKey() {         KeyPair keyPair;         try {             KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");             keyPairGenerator.initialize(2048);             keyPair = keyPairGenerator.generateKeyPair();         } catch (Exception ex) {             throw new IllegalStateException(ex);         }         return keyPair;     }      @Bean     public ProviderSettings providerSettings() {         return ProviderSettings.builder()                 .issuer("http://localhost:9000")                 .build();     } }

В authServerSecurityFilterChain настраиваем bean-компонент для применения безопасности OAuth по умолчанию и создадим страницу входа в форму.

В registeredClientRepository настраиваем репозиторий клиентских сервисов. В нашей архитектуре клиентом будет Spring Cloud Gateway, и соответственно здесь мы задаем интеграцию с ним:

  • Client ID — Spring будет использовать его для определения того, какой клиент пытается получить доступ к ресурсу.

  • Client secret code — секрет, известный клиенту и серверу, который обеспечивает доверие между ними.

  • Authentication method — в нашем случае мы будем использовать обычную аутентификацию, которая представляет собой просто имя пользователя и пароль.

  • Authorization grant type — мы хотим, чтобы клиент мог генерировать как код авторизации, так и токен обновления.

  • Redirect URI — клиент будет использовать его в потоке на основе перенаправления.

  • Scope — этот параметр определяет полномочия, которые может иметь клиент. В нашем случае у нас будет обязательный OidcScopes.OPENID и наш пользовательский resource.read.

В jwkSource настраиваем ключ подписи для токенов для сервера авторизации.

В providerSettings зададим URL-адрес, который провайдер будет использовать в качестве своего идентификатора.

Затем добавим дефолтный конфиг Spring Security:

@EnableWebSecurity public class DefaultSecurityConfig {      @Bean     SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {         http                 .authorizeRequests(authorizeRequests ->                         authorizeRequests                                 .anyRequest()                                 .authenticated()                 )                 .formLogin(withDefaults());         return http.build();     }      @Bean     UserDetailsService users() {         UserDetails user = User.withDefaultPasswordEncoder()                 .username("admin")                 .password("password")                 .roles("USER")                 .build();         return new InMemoryUserDetailsManager(user);     }  }

Здесь, в defaultSecurityFilterChain мы вызываем uthorizeRequests.anyRequest().authenticated(), чтобы требовать аутентификацию для всех запросов. Мы также предоставляем аутентификацию на основе форм, вызывая метод formLogin(defaults()).

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

На этом, с сервером авторизации всё.

Spring Cloud Gateway

Здесь будет происходить самое интересное. Помимо своей стандартной функции маршрутизации входящих запросов, Spring Cloud Gateway будет интегрирован с сервисом авторизации и будет реализовывать механизм Token Reley — как только сервер авторизации передаст шлюзу токен, шлюз помещает его в заголовок запроса к сервису, который проксирует. Для создания сервера нам потребуются следующие зависимости:

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency>   <groupId>org.springframework.cloud</groupId>   <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>

Далее настроим application.yml:

server:   port: 8080  spring:   cloud:     gateway:       routes:         - id: resource           uri: http://127.0.0.1:8090           predicates:             - Path=/resource           filters:             - TokenRelay=             - RemoveRequestHeader=Cookie    security:     oauth2:       client:         registration:           gateway:             provider: spring             client-id: gateway             client-secret: secret             authorization-grant-type: authorization_code             redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"             scope: openid,resource.read         provider:           spring:             issuer-uri: http://localhost:9000

В секцииspring.cloud.gateway.routes мы задаем конфигурацию маршрута до микросервиса ресурсов. Помимо собственно маршрута, здесь интересны два момента:

  • Фильтр - TokenRelay= задает бин TokenRelayGatewayFilterFactory в качестве фильтра в конфигурации маршрута для нашего сервера ресурсов, который будет осуществлять пересылку токена.

  • Фильтр - RemoveRequestHeader=Cookie сообщает шлюзу удалить за ненадобностью куки из запроса, для получения доступа нам достаточно токена.

В секции spring.oauth2.client.registration мы задаём интеграцию с Identity-провайдером, в нашем случае со Spring Authorization Server. Информация, указанная здесь, будет сопряжена с той, что мы указывали в бине RegisteredClientRepository, когда конфигурировали AuthorizationServerConfig при создании сервера авторизации, поэтому отдельно описывать каждый пункт не буду, они аналогичны. Дополню только, что в issuer-uri мы должны указать адрес нашего Identity-провайдера.

На этом здесь всё, переходим к серверу ресурсов.

Resource Server

Сервер ресурсов будет валидировать токен и отдавать данные по REST. Перейдем к созданию, нам потребуются следующие зависимости:

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-web</artifactId> </dependency>

Далее настроим application.yml:

server:   port: 8090  spring:   security:     oauth2:       resourceserver:         jwt:           issuer-uri: http://localhost:9000

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

Далее настроим WebSecurityConfig:

@EnableWebSecurity public class ResourceServerConfig {      @Bean     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {         http                 .mvcMatcher("/resource/**")                 .authorizeRequests()                 .mvcMatchers("/resource/**")                 .access("hasAuthority('SCOPE_resource.read')")                 .and()                 .oauth2ResourceServer()                 .jwt();         return http.build();     }  }

Здесь мы указываем, что каждый запрос к ресурсам должен быть авторизован и иметь права resource.read. oauth2ResourceServer() настраивает соединение с Identity-провайдером на основе данных, которые мы указали в application.yml.

Далее создаем REST контроллер:

@RestController public class ResourceController {      @GetMapping("/resource")     public String getResource() {         return "Resource";     } }

Здесь мы будем просто возвращать строку.

На этом всё, теперь запустим и посмотрим как это работает.

Запуск и тестирование:

Откроем браузер и перейдем по ссылке 127.0.0.1:8080/resource. Порт в URL указываем принадлежащий Gateway серверу. После перехода по ссылке нас редиректит на форму ввода логина и пароля:

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

На этом всё, надеюсь, было полезно, если есть какие-то замечания — пишите.

Для подготовки статьи использовались следующие материалы:

Ссылка проекта на GitHub


ссылка на оригинал статьи https://habr.com/ru/post/701912/


Комментарии

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

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