При реализации решения для аутентификации на основе Spring Security Reactive я столкнулся с проблемой, когда операции в цепочке в какой-то момент дублируются. От того все вызывалось дважды.
Виновником оказался оператор .transform в одной точке цепочки. После редактирования вызываемого метода и замены оператора на .flatMap проблема решилась и все вызывалось только один раз.
Согласно документация оператора,
function is applied to an original operator chain at assembly time to augment it with the encapsulated operators
и
is basically equivalent to chaining the operators directly.
Тогда почему оператор .transform инициировал вторую подписку на цепочку?
Этот поток проверки подлинности принимает имя доверенного пользователя и извлекает его данные из веб-службы.
Метод аутентификации для реализации ReactiveAuthenticationManager:
@Override
public Mono<Authentication> authenticate(Authentication providedAuthentication) {
String username = (String) providedAuthentication.getPrincipal();
String token = (String) providedAuthentication.getCredentials();
return Mono.just(providedAuthentication)
.doOnNext(x -> LOGGER.debug("Starting authentication of user {}", x))
.doOnNext(AuthenticationValidator.validateProvided)
.then(ReactiveSecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.flatMap(auth -> AuthenticationValidator.validateCoherence(auth, providedAuthentication))
.switchIfEmpty(Mono.defer(() -> {
LOGGER.trace("Switch if empty before retrieving user");
return retrieveUser(username, token);
}))
.doOnNext(logAccess);
}
Дублирование звонков началось от поставщика .switchIfEmpty до конца цепочки.
Метод создания Mono, используемый .switchIfEmpty:
private Mono<PreAuthenticatedAuthenticationToken> retrieveUser(String username, String token) {
return Mono.just(username)
.doOnNext(x -> LOGGER.trace("Before find by username"))
.then(habileUserDetails.findByUsername(username, token))
.cast(XXXUserDetails.class)
.transform(rolesProvider::provideFor)
.map(user -> new PreAuthenticatedAuthenticationToken(user, GlobalConfiguration.NO_CREDENTIAL, user.getAuthorities()))
.doOnNext(s -> LOGGER.debug("User data retrieved from XXX"));
}
Оператор .transform в строке 4 был заменен на .flatMap для решения проблемы.
Оригинальный метод, вызываемый оператором .transform:
public Mono<CompleteXXXUserDetails> provideFor(Mono<XXXUserDetails> user) {
return user
.map(XXXUserDetails::getAuthorities)
.map(l -> StreamHelper.transform(l, GrantedAuthority::getAuthority))
.map(matcher::match)
.map(enricher::enrich)
.map(l -> StreamHelper.transform(l, SimpleGrantedAuthority::new))
.zipWith(user, (authorities, userDetails)
-> CompleteXXXUserDetails.from(userDetails).withAllAuthorities(authorities));
}
Вот след казни:
DEBUG 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager : Starting authentication of user [REDACTED]
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager : Switch if empty before retrieving user
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager : Before find by username
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.xxx.user.UserRetriever : Between request and call
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.u.retriever.UserRetrieverV01: Calling webservice v01
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager : Before find by username
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.xxx.user.UserRetriever : Between request and call
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.u.retriever.UserRetrieverV01: Calling webservice v01
Для информации, я использую Spring Boot 2.1.2.RELEASE.




Этот ответ не устраняет основную причину, а скорее объясняет, как transform может применяться несколько раз при подписке на несколько раз, что не относится к проблеме OP. Изменил исходный текст в цитату.
That statement is only valid when the
transformis applied as a top-level operator in the chain you subscribe to. Here you are applying it withinretrieveUser, which is invoked inside aMono.defer(which goal is to execute that code for each different subscription). (edit:) so if thatdeferis subscribed to x times, the transformFunctionwill be applied x times as well.
composeis basicallytransform-inside-a-deferby the way.
transform не вызывает подписки, поэтому он должен исходить откуда-то еще... если вы посмотрите на источник, flux.transform(function) - это просто function.apply(flux). возможно, в этом свете вы сможете понять, почему применение функции provideFor приведет к «дополнительным подпискам». Кстати, вы можете показать, как вы узнали об этих дополнительных подписках?
Если вы посмотрите на трассировку выполнения, которую я предоставил, вы увидите, что последние 6 строк дублируются. В первом фрагменте кода вы можете увидеть строку журнала, которая соответствует второй строке трассировки выполнения. Что меня действительно беспокоит, так это то, что инструкция журнала, которая также есть в .defer, выполняется только один раз, а то, что после, выполняется дважды.
Вы также можете поделиться тем, как выглядел оригинальный provideFor?
Это оригинал, до того, как я исправил проблему. Я редактирую вопрос, чтобы быть более ясным.
настоящая проблема заключается в другом, добавил ответ
Проблема в том, что вы делаете user.whatever(...).zipWith(user, ...).
С преобразованием это переводится как:
Mono<XXXUserDetails> user = Mono.just(username)
.doOnNext(x -> LOGGER.trace("Before find by username"))
.then(habileUserDetails.findByUsername(username, token))
.cast(XXXUserDetails.class);
return user.wathewer(...)
.zipWith(user, ...);
Принимая во внимание, что с flatMap я предполагаю, что вы сделали что-то с эффектом flatMap(u -> provideFor(Mono.just(u))? Если это так, это будет означать:
Mono<XXXUserDetails> user = Mono.just(username)
.doOnNext(x -> LOGGER.trace("Before find by username"))
.then(habileUserDetails.findByUsername(username, token))
.cast(XXXUserDetails.class);
return user.flatMap(u -> {
Mono<XXXUserDetails> capture = Mono.just(u);
return capture.whatever(...)
.zipWith(capture, ...);
}
Вы можете видеть, как оба дважды подписываются на аMono<XXXUserDetails из-за zipWith.
Причина того, что кажется подписывается один раз с помощью flatMap, заключается в том, что он захватывает выходные данные восходящего конвейера и применяет функцию provideFor к этому захвату. Захват (Mono.just(u)) подписывается дважды, но действует как кеш и не несет никакой логики/логов/и т.д...
С transform захвата нет. Функция provideFor применяется непосредственно к восходящему конвейеру, что делает очевидным тот факт, что он подписывается дважды.
Спасибо Саймон. Имея это в виду, есть ли способ добиться ожидаемого поведения
.transformна других уровнях? Я понимаю, что инструкции.deferвыполняются при каждой подписке, но почему вообще существует дублированная подписка? Из-за поведения.transformнепредвиденный?