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