[case11] springsecurityresponsive gets security context.

  springsecurity

Order

This paper mainly studies the acquisition of spring security context under the reactive mode.

ReactiveSecurityContextHolder

Springboot2 supports asynchronous mode of webflux, so the traditional SecurityContextHolder based on threadlocal will not work. Spring security5.x also supports the reactive approach, which requires the use of the reactive version of SecurityContextHolder.

spring-security-core-5.0.3.RELEASE-sources.jar! /org/springframework/security/core/context/ReactiveSecurityContextHolder.java

public class ReactiveSecurityContextHolder {
    private static final Class<?> SECURITY_CONTEXT_KEY = SecurityContext.class;

    /**
     * Gets the {@code Mono<SecurityContext>} from Reactor {@link Context}
     * @return the {@code Mono<SecurityContext>}
     */
    public static Mono<SecurityContext> getContext() {
        return Mono.subscriberContext()
            .filter( c -> c.hasKey(SECURITY_CONTEXT_KEY))
            .flatMap( c-> c.<Mono<SecurityContext>>get(SECURITY_CONTEXT_KEY));
    }

    /**
     * Clears the {@code Mono<SecurityContext>} from Reactor {@link Context}
     * @return Return a {@code Mono<Void>} which only replays complete and error signals
     * from clearing the context.
     */
    public static Function<Context, Context> clearContext() {
        return context -> context.delete(SECURITY_CONTEXT_KEY);
    }

    /**
     * Creates a Reactor {@link Context} that contains the {@code Mono<SecurityContext>}
     * that can be merged into another {@link Context}
     * @param securityContext the {@code Mono<SecurityContext>} to set in the returned
     * Reactor {@link Context}
     * @return a Reactor {@link Context} that contains the {@code Mono<SecurityContext>}
     */
    public static Context withSecurityContext(Mono<? extends SecurityContext> securityContext) {
        return Context.of(SECURITY_CONTEXT_KEY, securityContext);
    }

    /**
     * A shortcut for {@link #withSecurityContext(Mono)}
     * @param authentication the {@link Authentication} to be used
     * @return a Reactor {@link Context} that contains the {@code Mono<SecurityContext>}
     */
    public static Context withAuthentication(Authentication authentication) {
        return withSecurityContext(Mono.just(new SecurityContextImpl(authentication)));
    }
}

As you can see, the context mechanism provided by reactor is used here to transfer variables of asynchronous threads.

Example

    public Mono<User> getCurrentUser() {
        return ReactiveSecurityContextHolder.getContext()
                .switchIfEmpty(Mono.error(new IllegalStateException("ReactiveSecurityContext is empty")))
                .map(SecurityContext::getAuthentication)
                .map(Authentication::getPrincipal)
                .cast(User.class);
    }

Source code analysis

ServerHttpSecurity

spring-security-config-5.0.3.RELEASE-sources.jar! /org/springframework/security/config/web/server/ServerHttpSecurity.java

    public SecurityWebFilterChain build() {
        if(this.built != null) {
            throw new IllegalStateException("This has already been built with the following stacktrace. " + buildToString());
        }
        this.built = new RuntimeException("First Build Invocation").fillInStackTrace();
        if(this.headers != null) {
            this.headers.configure(this);
        }
        WebFilter securityContextRepositoryWebFilter = securityContextRepositoryWebFilter();
        if(securityContextRepositoryWebFilter != null) {
            this.webFilters.add(securityContextRepositoryWebFilter);
        }
        if(this.csrf != null) {
            this.csrf.configure(this);
        }
        if(this.httpBasic != null) {
            this.httpBasic.authenticationManager(this.authenticationManager);
            this.httpBasic.configure(this);
        }
        if(this.formLogin != null) {
            this.formLogin.authenticationManager(this.authenticationManager);
            if(this.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.securityContextRepository);
            }
            if(this.formLogin.authenticationEntryPoint == null) {
                this.webFilters.add(new OrderedWebFilter(new LoginPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder()));
                this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder()));
            }
            this.formLogin.configure(this);
        }
        if(this.logout != null) {
            this.logout.configure(this);
        }
        this.requestCache.configure(this);
        this.addFilterAt(new SecurityContextServerWebExchangeWebFilter(), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE);
        if(this.authorizeExchange != null) {
            ServerAuthenticationEntryPoint authenticationEntryPoint = getAuthenticationEntryPoint();
            ExceptionTranslationWebFilter exceptionTranslationWebFilter = new ExceptionTranslationWebFilter();
            if(authenticationEntryPoint != null) {
                exceptionTranslationWebFilter.setAuthenticationEntryPoint(
                    authenticationEntryPoint);
            }
            this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION);
            this.authorizeExchange.configure(this);
        }
        AnnotationAwareOrderComparator.sort(this.webFilters);
        List<WebFilter> sortedWebFilters = new ArrayList<>();
        this.webFilters.forEach( f -> {
            if(f instanceof OrderedWebFilter) {
                f = ((OrderedWebFilter) f).webFilter;
            }
            sortedWebFilters.add(f);
        });
        return new MatcherSecurityWebFilterChain(getSecurityMatcher(), sortedWebFilters);
    }

The build method here creates securitycontextrepositorywebfilter and adds it to webFilters

securityContextRepositoryWebFilter

    private WebFilter securityContextRepositoryWebFilter() {
        ServerSecurityContextRepository repository = this.securityContextRepository;
        if(repository == null) {
            return null;
        }
        WebFilter result = new ReactorContextWebFilter(repository);
        return new OrderedWebFilter(result, SecurityWebFiltersOrder.REACTOR_CONTEXT.getOrder());
    }

ReactorContextWebFilter was created here

ReactorContextWebFilter

spring-security-web-5.0.3.RELEASE-sources.jar! /org/springframework/security/web/server/context/ReactorContextWebFilter.java

public class ReactorContextWebFilter implements WebFilter {
    private final ServerSecurityContextRepository repository;

    public ReactorContextWebFilter(ServerSecurityContextRepository repository) {
        Assert.notNull(repository, "repository cannot be null");
        this.repository = repository;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
            .subscriberContext(c -> c.hasKey(SecurityContext.class) ? c :
                withSecurityContext(c, exchange)
            );
    }

    private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) {
        return mainContext.putAll(this.repository.load(exchange)
            .as(ReactiveSecurityContextHolder::withSecurityContext));
    }
}

The SecurityContext is injected here using reactor’s subscriberContext.

Summary

Based on the context mechanism provided by reactor, spring security correspondingly provides ReactiveSecurityContextHolder to obtain current users, which is very convenient.

doc