Данная статья результат поиска некоего каноничного решения организации безопасности доступа к ресурсам в микросервисной архитектуре, построенной в экосистеме 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 серверу. После перехода по ссылке нас редиректит на форму ввода логина и пароля:

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

На этом всё, надеюсь, было полезно, если есть какие-то замечания — пишите.
Для подготовки статьи использовались следующие материалы:
ссылка на оригинал статьи https://habr.com/ru/post/701912/
Добавить комментарий