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
<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>Configuration Management
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]': DEBUGEntity, Repository, Service
@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();
}
}Sample data insertion is illustrated in the following image:
Unit Test
@SpringBootTest
class SpringBootWebfluxSecurity2ApplicationTests {
@Resource
private UsersService usersService;
@Test
public void testQueryUserByUsername() throws Exception {
usersService.queryUserByUsername("admin")
.doOnNext(System.out::println)
.subscribe();
System.in.read();
}
}Running the test produces logs similar to:
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]Reactive Security Configuration
@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();
}
}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.
public class ReactiveSecurityAutoConfiguration {
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {}
} EnableWebFluxSecurityimports two crucial configuration classes:
@Import({ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class, ReactiveOAuth2ClientImportSelector.class})
@Configuration
public @interface EnableWebFluxSecurity {} ServerHttpSecurityConfigurationcreates a ServerHttpSecurity bean that we customize in our ReactiveSecurityConfig.
@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();
}
} WebFluxSecurityConfigurationcollects all SecurityWebFilterChain beans and registers a WebFilterChainProxy that delegates to the matching chain.
@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;
}
}The WebFilterChainProxy iterates over the configured chains, finds the first that matches the request, and executes its filters.
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));
}
}The diagrams below illustrate the flow of WebFlux security processing:
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
