在实现基于Spring Security Reactive的身份验证解决方案时,我遇到了一个问题,即链中的操作有时会重复。从那以后,一切都被叫了两次。
罪魁祸首是链中某一点的操作员.transform
。编辑调用的方法并用.flatMap
替换运算符后,问题已解决,并且所有内容仅被调用了一次。
函数在组装时应用于原始运算符链,以使用封装的运算符对其进行扩充
和
基本上等效于直接链接运算符。
运营商.transform
为什么触发了对该链的第二次订阅,然后呢?
此身份验证流程将使用受信任的用户名,并从Web服务检索其详细信息。
实现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"));
}
第4行上的运算符.transform
已替换为.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。
答案 0 :(得分:1)
此答案并没有解决根本原因,而是说明了transform
在多次订阅后如何可以多次应用,而在OP的发行中则不是这样。将原始文本编辑成引号。
该语句仅在将
transform
用作您所订阅的链中的顶级运算符时才有效。在这里,您将在retrieveUser
内部调用的Mono.defer
中应用它(目标是为每个不同的订阅执行该代码)。 (编辑:),因此如果defer
被预订了x次,则转换Function
也将被应用x次。
compose
基本上是transform
-在一个defer
内部。
答案 1 :(得分:0)
问题在于您执行了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
,两个人都两次订阅了 a zipWith
。
原因是似乎使用flatMap
订阅一次,因为它捕获了上游管道的输出并在该捕获上应用了provideFor
函数。捕获(Mono.just(u)
)已订阅了两次,但充当了缓存并且不承担任何逻辑/日志/等等...
对于transform
,没有捕获。 provideFor
函数直接应用于上游管道,这使得它实际上两次订阅非常明显。