skip to content

How do you authorize specific @MessageMapping routes in RSocket — via the authorizePayload DSL vs @PreAuthorize method security — and what are the trade-offs?

level: seniorimportance: should knowfreq 25%

answer

  1. DSL: authorizePayload .route(pattern).hasRole
  2. method: @EnableReactiveMethodSecurity + @PreAuthorize
  3. DSL runs before handler; @PreAuthorize at invocation, argument-aware
  4. route delimiter '.' — patterns must match @MessageMapping
  5. first-match-wins in the DSL

basics

~10 s

You can authorize routes centrally in RSocketSecurity.authorizePayload using .route("pattern").hasRole(...), or on the handler with @PreAuthorize after enabling @EnableReactiveMethodSecurity. The DSL is connection-layer; method security is per-method and closer to the code.

solid answer

~40 s

Two complementary mechanisms. The payload interceptor approach declares rules in one place: authorizePayload(a -> a.route("admin.*").hasRole("ADMIN").route("chat.*").authenticated()...), matching routes with PathPattern/Ant syntax; it runs before the handler as part of the PayloadSocketAcceptorInterceptor. Method security uses @EnableReactiveMethodSecurity plus @PreAuthorize("hasRole('ADMIN')") (or @PreAuthorize with SpEL over method args) directly on the @MessageMapping method; it runs when the handler is invoked and can reference arguments and the principal. The DSL gives a centralized, route-pattern view and can protect at the connection boundary; method security co-locates rules with business logic and supports rich SpEL and argument-aware checks. Many apps use both: coarse route rules in the DSL, fine-grained @PreAuthorize on sensitive handlers. Both rely on the reactive security context populated by the authentication step.

code

java · 22 lines
java
@Configuration
@EnableRSocketSecurity
@EnableReactiveMethodSecurity   // enables @PreAuthorize on handlers
public class SecurityConfig {
    @Bean
    PayloadSocketAcceptorInterceptor interceptor(RSocketSecurity security) {
        return security.authorizePayload(a -> a
                .setup().authenticated()
                .route("admin.**").hasRole("ADMIN")   // coarse gate
                .anyRequest().authenticated()
                .anyExchange().permitAll())
            .simpleAuthentication(Customizer.withDefaults())
            .build();
    }
}

@Controller
class ChatController {
    @PreAuthorize("#roomId == authentication.name or hasRole('MOD')") // fine-grained
    @MessageMapping("chat.{roomId}.post")
    Mono<Void> post(@DestinationVariable String roomId, String msg) { /* ... */ }
}

go deeper

for a junior

Know routes can be protected with .route(...).hasRole in the DSL.

for a middle

Use both the authorizePayload DSL and @PreAuthorize, and know @EnableReactiveMethodSecurity is required.

for a senior

Compare central vs co-located authorization, argument-aware SpEL, ordering/first-match, and route-delimiter pitfalls.

for a principal

Establish a defense-in-depth authorization policy across routes, audit for uncovered routes, and standardize delimiter/matcher configuration platform-wide.

After authentication puts an `Authentication` into the reactive `SecurityContext`, you still must decide **who can call what**. RSocket routes are the strings a requester sends as routing metadata and that `@MessageMapping("...")` handlers match. There are two authorization layers: **1. Payload authorization DSL (`RSocketSecurity.authorizePayload`)** - Central, declarative rules evaluated by the `PayloadSocketAcceptorInterceptor` *before* the handler runs. - Matchers: `.setup()`, `.route("pattern")`, `.anyRequest()`, `.anyExchange()`. - Route patterns use Spring's `PathPattern`/Ant-style matching: `"admin.*"` matches one segment, `"admin.**"` matches many, delimiter is `.` by default (configurable via the `RSocketMessageHandler`'s route matcher). - Access rules: `.permitAll()`, `.denyAll()`, `.authenticated()`, `.hasRole("X")`, `.hasAuthority("SCOPE_x")`, `.hasAnyRole(...)`, `.access(customManager)`. - **First match wins**, top-down ordering, like `authorizeHttpRequests`. - Pros: one place to see the authorization map; protects at the messaging boundary; independent of handler code. - Cons: pattern-based only — cannot easily inspect message payload/arguments; another place to keep in sync with routes. **2. Method security (`@PreAuthorize` / `@PostAuthorize`)** - Enable with **`@EnableReactiveMethodSecurity`** on a config class (reactive stack). Then annotate `@MessageMapping` methods: ```java @PreAuthorize("hasRole('ADMIN')") @MessageMapping("admin.reset") Mono<Void> reset() { ... } ``` - Runs as an AOP interceptor when the handler is invoked. Supports **SpEL** including method arguments and `@AuthenticationPrincipal`: `@PreAuthorize("#tenantId == authentication.name")`. - Pros: co-located with business logic; argument-aware, fine-grained; reuses the same annotations as the rest of the app. - Cons: scattered across handlers; runs later (handler already dispatched); doesn't guard non-handler exchanges like setup. **Using both:** a common design is coarse route gating in `authorizePayload` (e.g. `.route("admin.**").hasRole("ADMIN")`) plus `@PreAuthorize` for object-level/argument-level checks on individual handlers. They stack: the DSL denies early; method security refines. **Gotchas:** - Route-pattern **delimiter**: RSocket routes commonly use `.` as separator; ensure your `.route("...")` patterns and `@MessageMapping` values agree, and that the `RSocketStrategies`/`RSocketMessageHandler` route matcher uses the expected separator. Mismatches silently fail to match, either denying or (worse) not matching a protective rule. - Method security requires **`@EnableReactiveMethodSecurity`**; the non-reactive `@EnableMethodSecurity`/`@EnableGlobalMethodSecurity` won't apply to reactive RSocket handlers. - `@PreAuthorize` on a handler returning `Mono/Flux` is evaluated reactively; blocking calls inside break the model. - If a route has no matching DSL rule and `.anyRequest()` is permissive, it's effectively public even if a handler exists — audit for gaps. - Both depend on the reactive security context being populated; without authentication configured, `authenticated()`/`hasRole` always fail (or the principal is anonymous). **When to use which:** simple role gating by route → DSL; checks that depend on message content, ownership, or complex SpEL → `@PreAuthorize`; regulated/high-value systems → both, defense-in-depth.

  • Why must you use @EnableReactiveMethodSecurity rather than @EnableMethodSecurity for RSocket handlers?
    RSocket handlers return Mono/Flux and run on the reactive stack; only @EnableReactiveMethodSecurity installs the reactive @PreAuthorize interceptor that reads the principal from the Reactor context. The servlet variant doesn't apply.
  • A route has a handler but no matching authorizePayload rule and .anyRequest().permitAll() — what's the risk?
    The route is effectively public: no authorization is enforced. You should audit for routes not covered by a protective rule and default .anyRequest() to authenticated() rather than permitAll().

saying these in an interview costs you the question

  • Using @EnableMethodSecurity (servlet) and expecting @PreAuthorize to work on reactive RSocket handlers
  • Assuming route patterns match with '/' when RSocket typically uses '.' as the delimiter
  • Thinking DSL and @PreAuthorize are mutually exclusive rather than layerable
  • Leaving .anyRequest().permitAll() so unmatched routes are silently open

context