You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Spring Boot - Interception of HTTP request headers at request upgrade/handshake #831
I was trying to implement the native browser Cookie/Session authentication to RSocket plugged to server at Spring Boot, and can't find easier way of how intercept the request headers passed before handshake process not using much reflection overriding and excluding of private classes. May be there is some easier way exists already that allows to configure a bean that will intercept request headers from client and can push connection to be closed with error if that is needed?
Also, may be it is even too much in this one thread, but would be good to have an option to add to response headers custom values exactly on the moment of HTTP request upgrade to WebSockets?
This will allow to implement native browser Cookie/Session authentication to RSocket and stop dealing with tokens. Like, browser makes a request to server to have it upgraded to websockets, server checks for cookie inside request headers, and if server allows, server will make a response to make request upgrade. But in situation, when cookies are missing some properties, server will pass additional cookie property with value to the upgrade response, or for example, pass property to upgrade response and not allow upgrade (so browser will got a cookie property with closed request), etc.
So, the easier way that allows just to pass a function, that will allow to check request headers, and ideally, add something more to upgrade response headers, would help a lot.
I had similar process implemented in basic Nodejs websocket server, so trying to implement same inside Java server with minimal overrides to native Java classes.
@maxim-bandurko-lsvt there have been a few changes for 1.0.1 that make this easier. Take a look at the current version of WebSocketHeadersSample. That should show you how to do this.
@maxim-bandurko-lsvt did you manage to implement such interceptor for Spring Boot? Started digging through the RSocket server code and Spring Security side, but haven't found an appropriate place for extending the functionality.
My goal was to not only to authenticate WS connection by headers/cookies, I was planing to populate the user id or whole user session object directly to @MessageMapping by passing the id into modified ConnectionSetupPayload that stores data inside socket for example. But still haven't implemented that yet.
Any way, at those class RSocketWebSocketNettyRouteProvider you can do all needed things for WebsocketInbound and WebsocketOutbound
@mmaask Well, I finally came to the moment that need to implement this Cookie/Session auth, and first way I did how I explained year ago in my comment above, but it turned to be not so nice in terms of implementation.
Biggest "stone" was spring-projects/spring-boot#18549 with not public RSocketWebSocketNettyRouteProvider. So had to follow the same way out that @FWinkler79 originally described by placing custom bean configuration in the same package as RSocketWebSocketNettyRouteProvider. So finally had something like:
But after that I had to store some auth status to be accessed by WebsocketDuplexConnection, and again one more stone was that it is final class, and can't extend it. So had to use a wrapper for it that wasn't very convenient also.
So, decided to switch to much easier implementation for right now by still keeping auth process per stream, but getting token from Cookie inside header (using Reflection as to get the needed private fields values):
Btw, it looks like having actual auth process would be easier to implement inside SocketAcceptorInterceptor and just store the auth status in some custom connection wrapper inside requester, so this way auth will be per connection and after that status that can be any Object can be re-used inside MessageMapping requesters (a little example discussed: #970 )
@mmaask Actually, decided to finish with implementation and do auth per connection.
So:
RSocketServerCustomizerCustom:
@Slf4j
@Configuration
public class RSocketServerCustomizerCustom implements RSocketServerCustomizer {
@Override
public void customize(RSocketServer rSocketServer) {
rSocketServer.interceptors(
(InterceptorRegistry configurer) -> {
log.info("Adding RSocket interceptors...");
configurer.forSocketAcceptor(new SocketAcceptorInterceptorCustom());
}
);
}
}
SocketAcceptorInterceptorCustom:
@Slf4j
public class SocketAcceptorInterceptorCustom implements SocketAcceptorInterceptor {
@Override
public SocketAcceptor apply(SocketAcceptor socketAcceptor) {
return (ConnectionSetupPayload setup, RSocket sendingSocket) -> {
try {
Field idcField = ReflectionUtils.findField(sendingSocket.getClass(), "connection");
idcField.setAccessible(true);
DuplexConnection idc = (DuplexConnection) ReflectionUtils.getField(idcField, sendingSocket);
Field shdcField = ReflectionUtils.findField(idc.getClass(), "source");
shdcField.setAccessible(true);
DuplexConnection shdc = (DuplexConnection) ReflectionUtils.getField(shdcField, idc);
Field wdcField = ReflectionUtils.findField(shdc.getClass(), "source");
wdcField.setAccessible(true);
WebsocketDuplexConnection wdc = (WebsocketDuplexConnection) ReflectionUtils.getField(wdcField, shdc);
WebsocketDuplexConnectionCustom wdcCustom = new WebsocketDuplexConnectionCustom(wdc);
synchronized(shdc){
ReflectionUtils.setField(wdcField, shdc, wdcCustom);
}
log.info("SocketAcceptorInterceptorCustom - header Cookie: " + wdcCustom.headers().get(HttpHeaders.COOKIE));
// Do some auth logic, can use setup payload also
//ConnAuthDTO authInfo = new ObjectMapper().readValue(setup.getDataUtf8(), ConnAuthDTO.class);
wdcCustom.setSession(true);
return socketAcceptor.accept(setup, sendingSocket);
} catch (Exception e) {
String errMsg = "try to accept a connection error.msg";
log.warn(errMsg, e);
return Mono.error(e);
}
};
}
}
It replaces existing WebsocketDuplexConnection wrapped with WebsocketDuplexConnectionCustom. And there you can do any Auth logic using headers or setup payload, and cache the session state value (can be any object) for reuse inside MessageMappings.
WebsocketDuplexConnectionCustom:
public class WebsocketDuplexConnectionCustom implements DuplexConnection {
private final WebsocketDuplexConnection websocketDuplexConnection;
private final Field websocketDuplexConnectionField;
private final Method doOnCloseMethod;
private AtomicReference<Object> session = new AtomicReference<Object>();
WebsocketDuplexConnectionCustom(WebsocketDuplexConnection websocketDuplexConnection){
this.websocketDuplexConnection = websocketDuplexConnection;
this.websocketDuplexConnectionField = ReflectionUtils.findField(websocketDuplexConnection.getClass(), "connection");
websocketDuplexConnectionField.setAccessible(true);
this.doOnCloseMethod = ReflectionUtils.findMethod(websocketDuplexConnection.getClass(), "doOnClose");
doOnCloseMethod.setAccessible(true);
session.set(null);
}
@Override
public ByteBufAllocator alloc() {
return websocketDuplexConnection.alloc();
}
@Override
public SocketAddress remoteAddress() {
return websocketDuplexConnection.remoteAddress();
}
//@Override
protected void doOnClose() {
ReflectionUtils.invokeMethod(doOnCloseMethod, websocketDuplexConnection);
}
@Override
public Flux<ByteBuf> receive() {
return websocketDuplexConnection.receive();
}
@Override
public void sendErrorAndClose(RSocketErrorException e) {
websocketDuplexConnection.sendErrorAndClose(e);
}
@Override
public Mono<Void> onClose() {
return websocketDuplexConnection.onClose();
}
@Override
public void dispose() {
websocketDuplexConnection.dispose();
}
@Override
public void sendFrame(int streamId, ByteBuf frame) {
websocketDuplexConnection.sendFrame(streamId, frame);
}
public Connection getConnection(){
return (Connection) ReflectionUtils.getField(websocketDuplexConnectionField, websocketDuplexConnection);
}
public HttpHeaders headers(){
WebsocketInbound wi = (WebsocketInbound) getConnection();
return wi.headers();
}
public void setSession(Object session){
this.session.set(session);
}
public Object getSession(){
return session.get();
}
}
Coming back to this because it is still the first thing you find when you want browser-cookie
authentication on RSocket over WebSocket in Spring Boot, and the answer it leaves you with is comment 1241021797 —
a walk through four private fields (connection → source → source → connection), a wrapper
substituted for the finalWebsocketDuplexConnection by writing it back through a private field,
and a bean planted in Spring's own package to replace the package-private RSocketWebSocketNettyRouteProvider.
Most of that is avoidable now. We shipped this, and measured each step against rsocket-core 1.1.5,
rsocket-transport-netty 1.1.5 and Spring Boot 4.1.0.
1. Reading the upgrade headers, and refusing the upgrade — published API, no reflection
HttpServer.childObserve sees the upgrade request with its headers and the connection carrying
it. Registered through Spring Boot's NettyServerCustomizer, so no package-private class is
involved and RSocketWebSocketNettyRouteProvider never has to be replaced:
@ComponentclassRSocketUpgradeObserverimplementsWebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
@Overridepublicvoidcustomize(NettyReactiveWebServerFactoryfactory) {
factory.addServerCustomizers(httpServer -> httpServer.childObserve((connection, state) -> {
if (state != ConnectionObserver.State.CONFIGURED) return;
if (!(connectioninstanceofHttpServerRequestrequest)) return;
if (!rsocketMappingPath.equals(request.fullPath())) return; // exact match, see noteHttpHeadersheaders = request.requestHeaders(); // Cookie, Origin, X-Forwarded-Forif (!allowed(headers.get(HttpHeaderNames.ORIGIN))) {
connection.dispose(); // refuse the upgradereturn;
}
deposit(connection.channel().remoteAddress(), contextFrom(headers));
}));
}
}
Two notes:
HttpServer.doOnRequest does not exist on this reactor-netty version; childObserve with the CONFIGURED guard and the instanceof HttpServerRequest narrowing is the hook that yields
request headers together with the connection.
Match the mapping path exactly. path.contains("/rs") also matches unrelated routes — ours
matched an HTTP endpoint whose path merely contained the string.
That alone answers the original question: check the cookie at the handshake, close the connection
if it fails. No reflection.
2. Getting that context onto the connection
The remaining problem is the handoff. childObserve knows the headers; the SocketAcceptor is
where you need them, and it is handed (ConnectionSetupPayload setup, RSocket sendingSocket) and
no address.
The key that bridges them is the TCP peer. The upgrade's SocketAddress and the address of the
accepted connection are the same value — a WebSocket upgrade is HTTP/1.1 and holds its connection
exclusively for its lifetime, so within that window the address names one connection and no other.
A deposit keyed on it, claimed once and evicted if unclaimed, is enough; no wrapper has to be
threaded through the framework.
We claim in a SocketAcceptorInterceptor rather than a DuplexConnectionInterceptor, because the
acceptor is the hook that wraps everything that follows — so it can also write the result into the
Reactor context, which is the only channel that reaches route authorization and @MessageMapping
handlers. A DuplexConnectionInterceptor at Type.SOURCE is handed the raw WebsocketDuplexConnection and can name the peer via the published DuplexConnection.remoteAddress(), but it has nowhere to put the answer.
3. The one hop that is still reflective, stated plainly
To claim by peer, the acceptor needs the peer, and it has to reach for it:
sendingSocket io.rsocket.core.RSocketRequester
└── DuplexConnection connection declared on RequesterResponderSupport
└── .remoteAddress() published method, published interface
One setAccessible, one field read. That is smaller than the walk in comment 1241021797 — one hop
instead of four, a read instead of substituting a wrapper for a final class, and nothing mutated —
but it is not zero, and it would be dishonest to present this as a fully public-API path.
Two things make it tolerable rather than merely smaller:
Locate the field by declared type, not by name. A name is an implementation detail that can
change silently; a field whose declared type is a published interface is much narrower to depend
on. A rename does not break it — only removing the field does, and that fails loudly.
Fence it with a test. Ours asserts the field resolves, so a framework upgrade that moves it
fails in CI. Without that the failure mode is severe: no peer means no claim, and a fail-closed
policy then refuses every connection in production.
4. What would close the last inch
Either would remove the reflection entirely:
expose the peer (or the DuplexConnection) to SocketAcceptor — it already has the RSocket
that holds it; or
expose the underlying channel on DuplexConnection, so a netty AttributeKey set at the upgrade
can be read at acceptance. That would remove the handoff registry as well, since the attribute
rides the channel itself.
Happy to open a separate issue for whichever of those looks more plausible.
Activity
rstoyanchev commentedon Jun 4, 2020
@maxim-bandurko-lsvt there have been a few changes for 1.0.1 that make this easier. Take a look at the current version of WebSocketHeadersSample. That should show you how to do this.
maxim-bandurko-lsvt commentedon Jun 4, 2020
@rstoyanchev Got it, thanks!
mmaask commentedon May 28, 2021
@maxim-bandurko-lsvt did you manage to implement such interceptor for Spring Boot? Started digging through the RSocket server code and Spring Security side, but haven't found an appropriate place for extending the functionality.
maxim-bandurko-lsvt commentedon Jun 7, 2021
@mmaask It depends on what exact level is needed to implement headers inside actual spring boot. I have very draft sample only for ws level that I implemented using my custom override built, because this class is still private:
https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java
Not a big deal, as I just make my own class that works how I need and overriding beans from:
https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java
My goal was to not only to authenticate WS connection by headers/cookies, I was planing to populate the user id or whole user session object directly to
@MessageMappingby passing the id into modifiedConnectionSetupPayloadthat stores data inside socket for example. But still haven't implemented that yet.Any way, at those class
RSocketWebSocketNettyRouteProvideryou can do all needed things forWebsocketInboundandWebsocketOutboundmaxim-bandurko-lsvt commentedon Sep 8, 2022
@mmaask Well, I finally came to the moment that need to implement this Cookie/Session auth, and first way I did how I explained year ago in my comment above, but it turned to be not so nice in terms of implementation.
Biggest "stone" was spring-projects/spring-boot#18549 with not public
RSocketWebSocketNettyRouteProvider. So had to follow the same way out that @FWinkler79 originally described by placing custom bean configuration in the same package asRSocketWebSocketNettyRouteProvider. So finally had something like:But after that I had to store some auth status to be accessed by
WebsocketDuplexConnection, and again one more stone was that it is final class, and can't extend it. So had to use a wrapper for it that wasn't very convenient also.So, decided to switch to much easier implementation for right now by still keeping auth process per stream, but getting token from Cookie inside header (using Reflection as to get the needed private fields values):
Btw, it looks like having actual auth process would be easier to implement inside
SocketAcceptorInterceptorand just store the auth status in some custom connection wrapper inside requester, so this way auth will be per connection and after that status that can be any Object can be re-used inside MessageMapping requesters (a little example discussed: #970 )maxim-bandurko-lsvt commentedon Sep 8, 2022
@mmaask Actually, decided to finish with implementation and do auth per connection.
So:
RSocketServerCustomizerCustom:SocketAcceptorInterceptorCustom:It replaces existing
WebsocketDuplexConnectionwrapped withWebsocketDuplexConnectionCustom. And there you can do any Auth logic using headers or setup payload, and cache the session state value (can be any object) for reuse inside MessageMappings.WebsocketDuplexConnectionCustom:spheenik commentedon Aug 7, 2026
Coming back to this because it is still the first thing you find when you want browser-cookie
authentication on RSocket over WebSocket in Spring Boot, and the answer it leaves you with is
comment 1241021797 —
a walk through four private fields (
connection→source→source→connection), a wrappersubstituted for the
finalWebsocketDuplexConnectionby writing it back through a private field,and a bean planted in Spring's own package to replace the package-private
RSocketWebSocketNettyRouteProvider.Most of that is avoidable now. We shipped this, and measured each step against rsocket-core 1.1.5,
rsocket-transport-netty 1.1.5 and Spring Boot 4.1.0.
1. Reading the upgrade headers, and refusing the upgrade — published API, no reflection
HttpServer.childObservesees the upgrade request with its headers and the connection carryingit. Registered through Spring Boot's
NettyServerCustomizer, so no package-private class isinvolved and
RSocketWebSocketNettyRouteProvidernever has to be replaced:Two notes:
HttpServer.doOnRequestdoes not exist on this reactor-netty version;childObservewith theCONFIGUREDguard and theinstanceof HttpServerRequestnarrowing is the hook that yieldsrequest headers together with the connection.
path.contains("/rs")also matches unrelated routes — oursmatched an HTTP endpoint whose path merely contained the string.
That alone answers the original question: check the cookie at the handshake, close the connection
if it fails. No reflection.
2. Getting that context onto the connection
The remaining problem is the handoff.
childObserveknows the headers; theSocketAcceptoriswhere you need them, and it is handed
(ConnectionSetupPayload setup, RSocket sendingSocket)andno address.
The key that bridges them is the TCP peer. The upgrade's
SocketAddressand the address of theaccepted connection are the same value — a WebSocket upgrade is HTTP/1.1 and holds its connection
exclusively for its lifetime, so within that window the address names one connection and no other.
A deposit keyed on it, claimed once and evicted if unclaimed, is enough; no wrapper has to be
threaded through the framework.
We claim in a
SocketAcceptorInterceptorrather than aDuplexConnectionInterceptor, because theacceptor is the hook that wraps everything that follows — so it can also write the result into the
Reactor context, which is the only channel that reaches route authorization and
@MessageMappinghandlers. A
DuplexConnectionInterceptoratType.SOURCEis handed the rawWebsocketDuplexConnectionand can name the peer via the publishedDuplexConnection.remoteAddress(), but it has nowhere to put the answer.3. The one hop that is still reflective, stated plainly
To claim by peer, the acceptor needs the peer, and it has to reach for it:
One
setAccessible, one field read. That is smaller than the walk in comment 1241021797 — one hopinstead of four, a read instead of substituting a wrapper for a
finalclass, and nothing mutated —but it is not zero, and it would be dishonest to present this as a fully public-API path.
Two things make it tolerable rather than merely smaller:
change silently; a field whose declared type is a published interface is much narrower to depend
on. A rename does not break it — only removing the field does, and that fails loudly.
fails in CI. Without that the failure mode is severe: no peer means no claim, and a fail-closed
policy then refuses every connection in production.
4. What would close the last inch
Either would remove the reflection entirely:
DuplexConnection) toSocketAcceptor— it already has theRSocketthat holds it; or
DuplexConnection, so a nettyAttributeKeyset at the upgradecan be read at acceptance. That would remove the handoff registry as well, since the attribute
rides the channel itself.
Happy to open a separate issue for whichever of those looks more plausible.