.transform / .compose使用Spring Security复制

时间:2019-02-05 13:16:20

标签: spring-boot spring-webflux project-reactor

在实现基于Spring Security Reactive的身份验证解决方案时,我遇到了一个问题,即链中的操作有时会重复。从那以后,一切都被叫了两次。

罪魁祸首是链中某一点的操作员.transform。编辑调用的方法并用.flatMap替换运算符后,问题已解决,并且所有内容仅被调用了一次。

问题

根据operator's documentation

  

函数在组装时应用于原始运算符链,以使用封装的运算符对其进行扩充

  

基本上等效于直接链接运算符。

运营商.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。

2 个答案:

答案 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函数直接应用于上游管道,这使得它实际上两次订阅非常明显。