Spring Security - довольно крутая штука, на тему которой много гайдов, статей на различных платформах. Но проблема в том, что множество этих видео ограничивается монолитной архитектурой. В этой статье я хочу рассказать о своем личном опыте применения ее для микросервисов. Конечно, это не статья уровня Тагира Валеева. Это исключительно личный опыт, которым хотелось бы поделиться, и может быть, кому то он окажется полезным.
В данной статье будет рассмотрено следующее:
Механизм регистрации и выдачи JWT токенов пользователям (кратко)
Механизм авторизации (кратко)
Security приложения на основании ролей пользователей
![](https://habrastorage.org/getpro/habr/upload_files/df8/7e2/e62/df87e2e622bc81f74807f4a895f1e8cd.png)
Применяемые технологии:
Spring Boot
Spring Cloud
Spring Security
JWT
WebFlux
Механика запросов, думаю, многим понятна. Если нет, картинка ниже вкратце все объяснит.
![](https://habrastorage.org/getpro/habr/upload_files/e88/b31/386/e88b31386f5eeb69fd87b29dabec74b0.png)
Приходит запрос от пользователя. Он перенаправляется на порт развернутого 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)
shamagulov
04.11.2022 23:49+1Представленная статья посвящена интересному направлению, обозначены наглядные примеры. Плюсом является наличие оригинальной инфографики механики запросов, а также то, что статья носит в большей мере практический характер в прикладной сфере.
Рекомендую данную статью к изучению.
kish4ever
Подскажите, почему вы не используете Spring Cloud Config чтобы можно было добавлять сервисы в api gateway security config в рантайме без переписывания кода приложения?
skorzanydran
Кмк, это не относится к статье. Здесь рассматривается авторизация, а не настройка конфигурации сервисов