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.
Environment
Spring Boot version 2.7.7.
Dependency Management
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>0.8.2.RELEASE</version>
</dependency></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:
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.