Spring Security - довольно крутая штука, на тему которой много гайдов, статей на различных платформах. Но проблема в том, что множество этих видео ограничивается монолитной архитектурой. В этой статье я хочу рассказать о своем личном опыте применения ее для микросервисов. Конечно, это не статья уровня Тагира Валеева. Это исключительно личный опыт, которым хотелось бы поделиться, и может быть, кому то он окажется полезным.

В данной статье будет рассмотрено следующее:

  • Механизм регистрации и выдачи JWT токенов пользователям (кратко)

  • Механизм авторизации (кратко)

  • Security приложения на основании ролей пользователей

Применяемые технологии:

  • Spring Boot

  • Spring Cloud

  • Spring Security

  • JWT

  • WebFlux

Механика запросов, думаю, многим понятна. Если нет, картинка ниже вкратце все объяснит.

Приходит запрос от пользователя. Он перенаправляется на порт развернутого Gateway, подставляется имя микросервиса, и далее идут обычный end-поинты указанного микросервиса. К примеру: localhost:8888/microserviceName/users.

Переходим к самому интересному!

Предлагаю немного пробежаться по микросервису регистрации, хранения пользователей в базе и выдачи JWT токенов. Предположим, что есть некая Person entity, в которой содержатся Id, username, password, role.

Метод создания пользователя из UserService:

public AuthResponse createPerson(PersonDto dto) {
        Person personEntity = mapper.dtoToPerson(dto);
        personEntity.setRole(Role.USER);
        personEntity.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt()));

        repository.save(personEntity);
        return getAuthResponse(personEntity);
    }

private AuthResponse getAuthResponse(Person personEntity) {
        String accessToken = jwt.generate(personEntity, accessType);
        String refreshToken = jwt.generate(personEntity, refreshType);
        return new AuthResponse(accessToken, refreshToken, personEntity.getMRID());
    }

Обратим внимание, что в строке №4 мы хэшим пароль и храним его в БД в зашифрованном виде. На тему генерации JWT токенов на просторах интернета множество полезных статей и видео. Данная же статья в большинстве своем посвящена Security нашего приложения.

Теперь перейдем к самому интересному. Api Gateway! На нем остановимся поподробнее.

Требуемые зависимости:

    implementation 'org.springframework.boot:spring-boot-starter-security:2.6.8'
    implementation 'org.springframework.boot:spring-boot-starter-webflux:2.6.8'
    implementation 'org.springframework:spring-webmvc:5.3.22'

    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.1'

Security config:

@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationManager authenticationManager;
    private final SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf()
                .disable()
                .authenticationManager(authenticationManager)
                .securityContextRepository(securityContextRepository)
                .authorizeExchange()
                .pathMatchers("/microservice1/users").permitAll()
                .pathMatchers("/microservice2/emails").authenticated()
                .pathMatchers("/microservice3/persons").hasAuthority("ADMIN")
                .anyExchange()
                .permitAll()
                .and()
                .httpBasic()
                .disable()
                .formLogin()
                .disable();
        return http.build();
    }
}

Заметьте, мы уже используем не@EnableWebSecurity,а @EnableWebFluxSecurity. Данная аннотация необходима, она позволяет нам реализовать Security в Gateway, реактивно бегая по микросервисам.

Как мы знаем, наследование WebSecurityConfigurerAdapter - deprecated. Поэтому реализуем SecurityWebFilterChain и опишем в нем требуемый функционал.

В 5,6 строках есть две важные штуки, а именно: authenticationManager, securityContextRepository.

Для начала рассмотрим SecurityContextRepository:

@Component
@RequiredArgsConstructor
public class SecurityContextRepository implements ServerSecurityContextRepository {

    private final AuthenticationManager authenticationManager;

    @Override
    public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange swe) {
        Mono<String> stringMono = Mono.justOrEmpty(swe.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
        return stringMono.flatMap(this::getSecurityContext);
    }

    private Mono<? extends SecurityContext> getSecurityContext(String token) {
        Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
        return authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
    }
}

Если вкратце ответить, что здесь происходит, то мы достаем из запроса Authorizarion header и отправляем его в метод authenticate из AuthenticationManager.

А вот и AuthenticationManager:

@Lazy
@Component
@RequiredArgsConstructor
public class AuthenticationManager implements ReactiveAuthenticationManager {

    private final Builder webClient;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String jwtToken = authentication.getCredentials().toString();
        return tokenValidate(jwtToken)
                .bodyToMono(UserAuthorities.class)
                .map(this::getAuthorities);
    }

    private UsernamePasswordAuthenticationToken getAuthorities(UserAuthorities userAuthorities) {
        return new UsernamePasswordAuthenticationToken(
                userAuthorities.getUsername(), null,
                userAuthorities.getAuthorities().stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList()));
    }

    private ResponseSpec tokenValidate(String token) {
        return webClient.build()
                .get()
                .uri(uriBuilder -> uriBuilder.host("registration").path("/token/auth").queryParam("token", token).build())
                .retrieve()
                .onStatus(HttpStatus.FORBIDDEN::equals, response -> Mono.error(new IllegalStateException("Token is not valid")));
    }
}

В методе tokenValidate мы отправляемся в микросервис registration, в endpoint token/auth. В нем должен быть реализован функционал проверки JWT токена. В нем вы должны брать все claims из JWT токена и записывать в DTO. Выглядит это, примерно, так:

public UserAuthorizationInfo getUserInfoFromToken(String token) {
        // здесь должна быть валидация вашего токена

        Claims allClaimsFromToken = jwt.getAllClaimsFromToken(token);
        UserAuthorizationInfo userInfo = new UserAuthorizationInfo();

        userInfo.setPersonId(allClaimsFromToken.get("id").toString());
        userInfo.setUsername(allClaimsFromToken.getSubject());

        List<String> authorities = new ArrayList<>();
        authorities.add(allClaimsFromToken.get(ROLES).toString());
        userInfo.setAuthorities(authorities);
        return userInfo;
    }

Далее мы получаем UserAuthorities, содержащую username и Collection<String> authorities. И по приходу запроса из header-а авторизации достаются username и роль. Теперь мы можем просто указывать, какие endpoint-ы кому доступны в Security Config из Api Gateway и все будет прекрасно работать, и кстати, довольно быстро, реактивщина ведь:)

Комментарии (3)


  1. kish4ever
    04.11.2022 08:59

    Подскажите, почему вы не используете Spring Cloud Config чтобы можно было добавлять сервисы в api gateway security config в рантайме без переписывания кода приложения?


    1. skorzanydran
      04.11.2022 14:31
      +1

      Кмк, это не относится к статье. Здесь рассматривается авторизация, а не настройка конфигурации сервисов


  1. shamagulov
    04.11.2022 23:49
    +1

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

    Рекомендую данную статью к изучению.