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/
Добавить комментарий