Support for reactive messaging applications using Project Reactor, providing reactive argument resolvers for injecting authentication into handler methods.
The reactive support package provides argument resolvers for reactive message handlers that return Mono or Flux. These are used in reactive messaging applications built with Project Reactor.
Package: org.springframework.security.messaging.handler.invocation.reactive
Resolves @AuthenticationPrincipal annotated parameters for reactive message handlers, returning the authenticated principal wrapped in a Mono.
/**
* Reactive HandlerMethodArgumentResolver for @AuthenticationPrincipal parameters.
* Returns Mono<Object> to support reactive message handling.
*
* @since 5.2
*/
public class AuthenticationPrincipalArgumentResolver
implements HandlerMethodArgumentResolver {
/**
* Sets the BeanResolver for resolving beans in SpEL expressions.
*
* @param beanResolver the bean resolver
*/
public void setBeanResolver(BeanResolver beanResolver);
/**
* Sets the ReactiveAdapterRegistry for reactive type conversions.
*
* @param adapterRegistry the adapter registry
*/
public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry);
/**
* Checks if this resolver supports the given parameter.
* Supports parameters annotated with @AuthenticationPrincipal.
*
* @param parameter the method parameter
* @return true if parameter is annotated with @AuthenticationPrincipal
*/
public boolean supportsParameter(MethodParameter parameter);
/**
* Resolves the authenticated principal reactively from ReactiveSecurityContextHolder.
*
* @param parameter the method parameter
* @param message the current message
* @return Mono containing the authenticated principal
*/
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message);
/**
* Sets template defaults for annotation expression evaluation.
*
* @param templateDefaults the template defaults
* @since 6.4
*/
public void setTemplateDefaults(
AnnotationTemplateExpressionDefaults templateDefaults
);
}Usage Example:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.invocation.reactive.ArgumentResolverConfigurer;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Mono;
@Configuration
public class ReactiveMessageConfig {
@Bean
public ArgumentResolverConfigurer argumentResolverConfigurer() {
ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
return configurer;
}
}
@Controller
public class ReactiveMessageHandler {
@MessageMapping("chat.message")
public Mono<Void> handleMessage(
@AuthenticationPrincipal Mono<UserDetails> userMono,
Mono<String> messageMono
) {
return Mono.zip(userMono, messageMono)
.flatMap(tuple -> {
UserDetails user = tuple.getT1();
String message = tuple.getT2();
// Process reactive message with authenticated user
return processMessage(user.getUsername(), message);
});
}
@MessageMapping("profile.update")
public Mono<Void> handleProfileUpdate(
@AuthenticationPrincipal(expression = "username") Mono<String> usernameMono,
Mono<ProfileUpdate> updateMono
) {
return Mono.zip(usernameMono, updateMono)
.flatMap(tuple -> {
String username = tuple.getT1();
ProfileUpdate update = tuple.getT2();
return updateProfile(username, update);
});
}
private Mono<Void> processMessage(String username, String message) {
// Implementation
return Mono.empty();
}
private Mono<Void> updateProfile(String username, ProfileUpdate update) {
// Implementation
return Mono.empty();
}
}Resolves @CurrentSecurityContext annotated parameters for reactive message handlers, providing access to the full SecurityContext.
/**
* Reactive HandlerMethodArgumentResolver for @CurrentSecurityContext parameters.
* Returns Mono<SecurityContext> to support reactive message handling.
*
* @since 5.2
*/
public class CurrentSecurityContextArgumentResolver
implements HandlerMethodArgumentResolver {
/**
* Sets the BeanResolver for resolving beans in SpEL expressions.
*
* @param beanResolver the bean resolver
*/
public void setBeanResolver(BeanResolver beanResolver);
/**
* Sets the ReactiveAdapterRegistry for reactive type conversions.
*
* @param adapterRegistry the adapter registry
*/
public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry);
/**
* Checks if this resolver supports the given parameter.
* Supports parameters annotated with @CurrentSecurityContext or Mono<SecurityContext>.
*
* @param parameter the method parameter
* @return true if parameter is supported
*/
public boolean supportsParameter(MethodParameter parameter);
/**
* Resolves the SecurityContext reactively from ReactiveSecurityContextHolder.
*
* @param parameter the method parameter
* @param message the current message
* @return Mono containing the SecurityContext
*/
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message);
/**
* Sets template defaults for annotation expression evaluation.
*
* @param templateDefaults the template defaults
* @since 6.4
*/
public void setTemplateDefaults(
AnnotationTemplateExpressionDefaults templateDefaults
);
}Usage Example:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.CurrentSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Mono;
@Controller
public class ReactiveSecurityContextHandler {
@MessageMapping("admin.action")
public Mono<Void> handleAdminAction(
@CurrentSecurityContext Mono<SecurityContext> contextMono,
Mono<AdminAction> actionMono
) {
return Mono.zip(contextMono, actionMono)
.flatMap(tuple -> {
SecurityContext context = tuple.getT1();
AdminAction action = tuple.getT2();
Authentication auth = context.getAuthentication();
// Perform action with full security context
return performAction(auth, action);
});
}
@MessageMapping("audit.log")
public Mono<Void> handleAuditLog(
@CurrentSecurityContext(expression = "authentication")
Mono<Authentication> authMono,
Mono<AuditEntry> entryMono
) {
return Mono.zip(authMono, entryMono)
.flatMap(tuple -> {
Authentication auth = tuple.getT1();
AuditEntry entry = tuple.getT2();
// Log with authentication info
return logAuditEntry(auth.getName(), entry);
});
}
// Direct Mono<SecurityContext> parameter (no annotation needed)
@MessageMapping("security.check")
public Mono<Boolean> checkSecurity(Mono<SecurityContext> contextMono) {
return contextMono
.map(context -> context.getAuthentication() != null &&
context.getAuthentication().isAuthenticated());
}
private Mono<Void> performAction(Authentication auth, AdminAction action) {
// Implementation
return Mono.empty();
}
private Mono<Void> logAuditEntry(String username, AuditEntry entry) {
// Implementation
return Mono.empty();
}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.reactive.ArgumentResolverConfigurer;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.handler.invocation.reactive.CurrentSecurityContextArgumentResolver;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
@Configuration
@EnableRSocketSecurity
public class ReactiveMessagingSecurityConfig {
@Bean
public PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
security
.authorizePayload(authorize -> authorize
.route("chat.**").authenticated()
.route("admin.**").hasRole("ADMIN")
.anyExchange().permitAll()
)
.simpleAuthentication(Customizer.withDefaults());
return security.build();
}
@Bean
public RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.setRSocketStrategies(strategies);
// Add reactive argument resolvers
ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
configurer.addCustomResolver(new CurrentSecurityContextArgumentResolver());
handler.setArgumentResolverConfigurer(configurer);
return handler;
}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.reactive.ArgumentResolverConfigurer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.handler.invocation.reactive.CurrentSecurityContextArgumentResolver;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ReactiveWebSocketConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/ws/**").authenticated()
.anyExchange().permitAll()
)
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public HandlerMapping webSocketHandlerMapping(WebSocketHandler wsHandler) {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/ws/chat", wsHandler);
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(1);
handlerMapping.setUrlMap(map);
return handlerMapping;
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
@Bean
public ArgumentResolverConfigurer argumentResolverConfigurer() {
ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
configurer.addCustomResolver(new AuthenticationPrincipalArgumentResolver());
configurer.addCustomResolver(new CurrentSecurityContextArgumentResolver());
return configurer;
}
}@Controller
public class ChatController {
@MessageMapping("chat.send")
public Mono<ChatMessage> sendMessage(
@AuthenticationPrincipal Mono<UserDetails> userMono,
Mono<String> contentMono
) {
return Mono.zip(userMono, contentMono)
.map(tuple -> {
UserDetails user = tuple.getT1();
String content = tuple.getT2();
return new ChatMessage(user.getUsername(), content);
});
}
}@Controller
public class UserController {
@MessageMapping("user.update")
public Mono<Void> updateUser(
@AuthenticationPrincipal(expression = "username") Mono<String> usernameMono,
Mono<UserUpdate> updateMono
) {
return Mono.zip(usernameMono, updateMono)
.flatMap(tuple -> userService.update(tuple.getT1(), tuple.getT2()));
}
}@Controller
public class AuditController {
@MessageMapping("audit.create")
public Mono<Void> createAuditEntry(
@CurrentSecurityContext Mono<SecurityContext> contextMono,
Mono<AuditData> dataMono
) {
return Mono.zip(contextMono, dataMono)
.flatMap(tuple -> {
SecurityContext context = tuple.getT1();
AuditData data = tuple.getT2();
Authentication auth = context.getAuthentication();
return auditService.create(auth, data);
});
}
}public class CustomUserDetails implements UserDetails {
private String username;
private String email;
private Long userId;
// ... getters and UserDetails methods
}
@Controller
public class ProfileController {
@MessageMapping("profile.get")
public Mono<Profile> getProfile(
@AuthenticationPrincipal Mono<CustomUserDetails> userMono
) {
return userMono
.flatMap(user -> profileService.getProfile(user.getUserId()));
}
@MessageMapping("profile.email")
public Mono<String> getEmail(
@AuthenticationPrincipal(expression = "email") Mono<String> emailMono
) {
return emailMono;
}
}@Controller
public class CollaborationController {
@MessageMapping("collab.join")
public Mono<CollaborationSession> joinSession(
@AuthenticationPrincipal Mono<UserDetails> userMono,
@CurrentSecurityContext(expression = "authentication.authorities")
Mono<Collection<? extends GrantedAuthority>> authoritiesMono,
Mono<String> sessionIdMono
) {
return Mono.zip(userMono, authoritiesMono, sessionIdMono)
.flatMap(tuple -> {
UserDetails user = tuple.getT1();
Collection<? extends GrantedAuthority> authorities = tuple.getT2();
String sessionId = tuple.getT3();
return collaborationService.join(
user.getUsername(),
authorities,
sessionId
);
});
}
}@Controller
public class SecureController {
@MessageMapping("secure.action")
public Mono<Response> secureAction(
@AuthenticationPrincipal Mono<UserDetails> userMono,
Mono<ActionRequest> requestMono
) {
return Mono.zip(userMono, requestMono)
.flatMap(tuple -> {
UserDetails user = tuple.getT1();
ActionRequest request = tuple.getT2();
return performAction(user, request);
})
.onErrorResume(AccessDeniedException.class, e ->
Mono.just(Response.denied("Access denied"))
)
.onErrorResume(AuthenticationException.class, e ->
Mono.just(Response.error("Authentication failed"))
);
}
private Mono<Response> performAction(UserDetails user, ActionRequest request) {
// Implementation
return Mono.just(Response.success());
}
static class Response {
static Response denied(String message) { return new Response(); }
static Response error(String message) { return new Response(); }
static Response success() { return new Response(); }
}
}The reactive argument resolvers obtain authentication from ReactiveSecurityContextHolder. In reactive applications, security context is typically stored in Reactor's context.
Example of setting security context:
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import reactor.core.publisher.Mono;
public Mono<Void> processWithAuth(Authentication authentication) {
SecurityContext context = // ... create security context with authentication
return someReactiveOperation()
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context)));
}| Feature | Imperative | Reactive |
|---|---|---|
| Return Type | Object | Mono<Object> |
| Package | .context | .handler.invocation.reactive |
| Context Storage | SecurityContextHolder | ReactiveSecurityContextHolder |
| Thread Model | ThreadLocal | Reactor Context |
| Use Case | STOMP/WebSocket | RSocket, WebFlux |
Reactive support requires:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>Reactive resolvers may emit null if no authentication is available:
@MessageMapping("secure.action")
public Mono<Void> handleAction(
@AuthenticationPrincipal Mono<UserDetails> userMono
) {
return userMono
.switchIfEmpty(Mono.error(new AuthenticationException("Not authenticated")))
.flatMap(user -> {
// Process with authenticated user
return performAction(user);
});
}Always handle errors in reactive chains:
@MessageMapping("secure.operation")
public Mono<Response> handleOperation(
@AuthenticationPrincipal Mono<UserDetails> userMono,
Mono<Request> requestMono
) {
return Mono.zip(userMono, requestMono)
.flatMap(tuple -> processRequest(tuple.getT1(), tuple.getT2()))
.onErrorResume(AuthenticationException.class, e -> {
logger.warn("Authentication failed", e);
return Mono.just(Response.error("Authentication required"));
})
.onErrorResume(AccessDeniedException.class, e -> {
logger.warn("Access denied", e);
return Mono.just(Response.denied("Access denied"));
})
.onErrorResume(Exception.class, e -> {
logger.error("Unexpected error", e);
return Mono.just(Response.error("Internal error"));
});
}Causes:
Solutions:
// Ensure security context is propagated
return someReactiveOperation()
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(
Mono.just(securityContext)
));
// Check if authentication is available
@MessageMapping("check")
public Mono<Boolean> checkAuth(
@AuthenticationPrincipal Mono<UserDetails> userMono
) {
return userMono
.hasElement() // Check if Mono has value
.map(hasAuth -> {
logger.debug("Has authentication: {}", hasAuth);
return hasAuth;
});
}Causes:
Solutions:
// Configure bean resolver
AuthenticationPrincipalArgumentResolver resolver =
new AuthenticationPrincipalArgumentResolver();
resolver.setBeanResolver(applicationContext);
// Validate expression syntax
try {
Expression expr = parser.parseExpression("username");
// Expression is valid
} catch (SpELParseException e) {
logger.error("Invalid expression", e);
}