Security, Cloud с JWT и WebFlux

от автора

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 и все будет прекрасно работать, и кстати, довольно быстро, реактивщина ведь:)


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


Комментарии

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

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