Information Security 10 min read

How to Secure a Spring Boot WebFlux Application with Reactive Security

This guide walks through setting up a Spring Boot 2.7.7 WebFlux project, configuring R2DBC dependencies, defining entity, repository and service layers, writing unit tests, and implementing a full reactive security configuration using WebFlux filters and custom access rules.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Secure a Spring Boot WebFlux Application with Reactive Security

Environment

Spring Boot version 2.7.7.

Dependency Management

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-data-r2dbc&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;dev.miku&lt;/groupId&gt;
  &lt;artifactId&gt;r2dbc-mysql&lt;/artifactId&gt;
  &lt;version&gt;0.8.2.RELEASE&lt;/version&gt;
&lt;/dependency&gt;</code>

Configuration Management

<code>spring:
  r2dbc:
    url: r2dbc:mysql://localhost:3306/testjpa?serverZoneId=GMT%2B8
    username: root
    password: 123123
    pool:
      initialSize: 100
      maxSize: 200
      maxCreateConnectionTime: 30s
logging:
  level:
    '[org.springframework.r2dbc]': DEBUG</code>

Entity, Repository, Service

<code>@Table("t_users")
public class Users implements UserDetails {
    @Id
    private Integer id;
    private String username;
    private String password;
}

public interface UsersRepository extends ReactiveSortingRepository<Users, String> {
    Mono<Users> findByUsername(String username);
}

@Service
public class UsersService {
    @Resource
    private R2dbcEntityTemplate template;
    @Resource
    private UsersRepository ur;

    public Mono<Users> queryUserByUsername(String username) {
        return ur.findByUsername(username);
    }

    public Mono<Users> queryUserByUsernameAndPassword(String username, String password) {
        return ur.findByUsernameAndPassword(username, password);
    }

    public Flux<Users> queryUsers() {
        return template.select(Users.class).all();
    }
}</code>

Sample data insertion is illustrated in the following image:

Unit Test

<code>@SpringBootTest
class SpringBootWebfluxSecurity2ApplicationTests {
    @Resource
    private UsersService usersService;

    @Test
    public void testQueryUserByUsername() throws Exception {
        usersService.queryUserByUsername("admin")
            .doOnNext(System.out::println)
            .subscribe();
        System.in.read();
    }
}</code>

Running the test produces logs similar to:

<code>2023-01-12 17:43:48.863 DEBUG --- [main] o.s.w.r.r.m.a.ControllerMethodResolver : ControllerAdvice beans: none
2023-01-12 17:43:48.896 DEBUG --- [main] o.s.w.s.adapter.HttpWebHandlerAdapter : enableLoggingRequestDetails='false': form data and headers will be masked to prevent unsafe logging of potentially sensitive data
2023-01-12 17:43:49.147  INFO --- [main] ringBootWebfluxSecurity2ApplicationTests : Started SpringBootWebfluxSecurity2ApplicationTests in 1.778 seconds (JVM running for 2.356)
2023-01-12 17:43:50.141 DEBUG --- [actor-tcp-nio-2] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT t_users.id, t_users.username, t_users.password FROM t_users WHERE t_users.username = ?]
Users [id=3, username=admin, password=123123]</code>

Reactive Security Configuration

<code>@Configuration
@EnableReactiveMethodSecurity
public class ReactiveSecurityConfig {
    // ReactiveUserDetailsService based on username lookup
    @Bean
    public ReactiveUserDetailsService reativeUserDetailsService(UsersService usersService) {
        return username -> usersService.queryUserByUsername(username)
            .map(user -> (UserDetails) user);
    }

    // Simple plain‑text password encoder (no hashing)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return rawPassword.toString().equals(encodedPassword);
            }
        };
    }

    @Order(Ordered.HIGHEST_PRECEDENCE + 1)
    @Bean
    public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange(authorize -> authorize
                .pathMatchers("/resources/**", "/favicon.ico").permitAll()
                .pathMatchers("/users/**").hasRole("ADMIN")
                .pathMatchers("/api/**").access((authentication, context) -> {
                    return authentication.map(auth -> {
                        if ("user1".equals(auth.getName())) {
                            return new AuthorizationDecision(false);
                        }
                        MultiValueMap<String, String> params = context.getExchange().getRequest().getQueryParams();
                        List<String> sk = params.get("sk");
                        if (sk == null || sk.get(0).equals("u")) {
                            return new AuthorizationDecision(false);
                        }
                        return new AuthorizationDecision(true);
                    });
                }).anyExchange().authenticated());

        // Exception handling
        http.exceptionHandling(execSpec -> {
            execSpec.accessDeniedHandler((exchange, denied) -> {
                ServerHttpResponse response = exchange.getResponse();
                response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
                DataBuffer body = response.bufferFactory().allocateBuffer();
                body.write("无权限 - " + denied.getMessage(), StandardCharsets.UTF_8);
                return response.writeWith(Mono.just(body));
            });
            execSpec.authenticationEntryPoint((exchange, ex) -> {
                ServerHttpResponse response = exchange.getResponse();
                response.getHeaders().add("Content-Type", "text/html;charset=UTF-8");
                DataBuffer body = response.bufferFactory().allocateBuffer();
                body.write("请先登录 - " + ex.getMessage(), StandardCharsets.UTF_8);
                return response.writeWith(Mono.just(body));
            });
        });
        http.csrf(csrf -> csrf.disable());
        http.formLogin();
        return http.build();
    }
}
</code>

Underlying Principle

The core of the reactive security setup is a WebFilter that intercepts requests.

Spring Boot auto‑configuration provides ReactiveSecurityAutoConfiguration , which imports EnableWebFluxSecurity to activate security features.

<code>public class ReactiveSecurityAutoConfiguration {
    @EnableWebFluxSecurity
    static class EnableWebFluxSecurityConfiguration {}
}</code>

EnableWebFluxSecurity imports two crucial configuration classes:

<code>@Import({ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class, ReactiveOAuth2ClientImportSelector.class})
@Configuration
public @interface EnableWebFluxSecurity {}
</code>

ServerHttpSecurityConfiguration creates a ServerHttpSecurity bean that we customize in our ReactiveSecurityConfig .

<code>@Configuration(proxyBeanMethods = false)
class ServerHttpSecurityConfiguration {
    @Bean(HTTPSECURITY_BEAN_NAME)
    @Scope("prototype")
    ServerHttpSecurity httpSecurity() {
        ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
        return http.authenticationManager(authenticationManager())
            .headers().and()
            .logout().and();
    }
}
</code>

WebFluxSecurityConfiguration collects all SecurityWebFilterChain beans and registers a WebFilterChainProxy that delegates to the matching chain.

<code>@Configuration(proxyBeanMethods = false)
class WebFluxSecurityConfiguration {
    private List<SecurityWebFilterChain> securityWebFilterChains;
    @Autowired(required = false)
    void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) {
        this.securityWebFilterChains = securityWebFilterChains;
    }
    @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
    @Order(WEB_FILTER_CHAIN_FILTER_ORDER)
    WebFilterChainProxy springSecurityWebFilterChainFilter() {
        return new WebFilterChainProxy(getSecurityWebFilterChains());
    }
    private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
        return ObjectUtils.isEmpty(securityWebFilterChains) ?
            Arrays.asList(springSecurityFilterChain()) : securityWebFilterChains;
    }
}
</code>

The WebFilterChainProxy iterates over the configured chains, finds the first that matches the request, and executes its filters.

<code>public class WebFilterChainProxy implements WebFilter {
    private final List<SecurityWebFilterChain> filters;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Flux.fromIterable(this.filters)
            .filterWhen(securityWebFilterChain -> securityWebFilterChain.matches(exchange)).next()
            .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
            .flatMap(securityWebFilterChain -> securityWebFilterChain.getWebFilters().collectList())
            .map(filters -> new FilteringWebHandler(chain::filter, filters))
            .map(DefaultWebFilterChain::new)
            .flatMap(securedChain -> securedChain.filter(exchange));
    }
}
</code>

The diagrams below illustrate the flow of WebFlux security processing:

JavaSpring BootWebFluxR2DBCreactive-security
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.