3

I'm trying to get the the Principal user name from Spring websocket SessionConnectEvent but it is null on every listener. What I can be doing wrong?

To implement it I followed the answers you will find here: how to capture connection event in my webSocket server with Spring 4?

@Slf4j
@Service
public class SessionEventListener {

    @EventListener
    private void handleSessionConnect(SessionConnectEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        String sessionId = headers.getSessionId();
        log.debug("sessionId is " + sessionId);
        String username = headers.getUser().getName(); // headers.getUser() is null
        log.debug("username is " + username);
    }

    @EventListener
    private void handleSessionConnected(SessionConnectEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        String sessionId = headers.getSessionId();
        log.debug("sessionId is " + sessionId);
        String username = headers.getUser().getName(); // headers.getUser() is null
        log.debug("username is " + username);
    }

    @EventListener
    private void handleSubscribeEvent(SessionSubscribeEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        String sessionId = headers.getSessionId();
        log.debug("sessionId is " + sessionId);
        String subscriptionId = headers.getSubscriptionId();
        log.debug("subscriptionId is " + subscriptionId);
        String username = headers.getUser().getName(); // headers.getUser() is null
        log.debug("username is " + username);
    }

    @EventListener
    private void handleUnsubscribeEvent(SessionUnsubscribeEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        String sessionId = headers.getSessionId();
        log.debug("sessionId is " + sessionId);
        String subscriptionId = headers.getSubscriptionId();
        log.debug("subscriptionId is " + subscriptionId);
        String username = headers.getUser().getName(); // headers.getUser() is null
        log.debug("username is " + username);
    }

    @EventListener
    private void handleSessionDisconnect(SessionDisconnectEvent event) {
        SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
        log.debug("sessionId is " + event.getSessionId());
        String username = headers.getUser().getName(); // headers.getUser() is null
        log.debug("username is " + username);
    }

}

This is my security config:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and().csrf().disable();
    }
}
abarazal
  • 369
  • 6
  • 19

2 Answers2

5

As I'm not implementing an authentication mechanisim, Spring has no enough information to provide a Principal user name. So what I had to do is to configure a HandshakeHandler that generates the Principal.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    public static final String ENDPOINT_CONNECT = "/connect";
    public static final String SUBSCRIBE_USER_PREFIX = "/private";
    public static final String SUBSCRIBE_USER_REPLY = "/reply";
    public static final String SUBSCRIBE_QUEUE = "/queue";

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker(SUBSCRIBE_QUEUE, SUBSCRIBE_USER_REPLY);
        registry.setUserDestinationPrefix(SUBSCRIBE_USER_PREFIX);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(ENDPOINT_CONNECT)
                // assign a random username as principal for each websocket client
                // this is needed to be able to communicate with a specific client
                .setHandshakeHandler(new AssignPrincipalHandshakeHandler())
                .setAllowedOrigins("*");
    }

}
/**
 * Assign a random username as principal for each websocket client. This is
 * needed to be able to communicate with a specific client.
 */
public class AssignPrincipalHandshakeHandler extends DefaultHandshakeHandler {
    private static final String ATTR_PRINCIPAL = "__principal__";

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
        final String name;
        if (!attributes.containsKey(ATTR_PRINCIPAL)) {
            name = generateRandomUsername();
            attributes.put(ATTR_PRINCIPAL, name);
        } else {
            name = (String) attributes.get(ATTR_PRINCIPAL);
        }
        return new Principal() {
            @Override
            public String getName() {
                return name;
            }
        };
    }

    private String generateRandomUsername() {
        RandomStringGenerator randomStringGenerator = 
                new RandomStringGenerator.Builder()
                    .withinRange('0', 'z')
                    .filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS).build();
        return randomStringGenerator.generate(32);
    }
}
abarazal
  • 369
  • 6
  • 19
  • That's useless as it's not setting the actual connected user, just generating a random String and setting into name field – FARS Aug 07 '23 at 15:17
  • @FARS If you require the actual username, you need to configure Spring Security. – abarazal Aug 14 '23 at 16:23
0

Looking into the implementation of AbstractSubProtocolEvent (the superclass of all the events you're interested in) you can see that the user is hold in a seperate field. So you can simply access the user by calling event.getUser(). You don't need to get it from the message.

E.g. for the SessionConnectedEvent you can see that the user gets populated in the event but not the message.

Update:

You can only access the user when you authenticated the http upgrade. So you need to have a WebSecurityConfigurerAdapter that configures something like:

@Configuration
public static class UserWebSecurity extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers(WebsocketPaths.WEBSOCKET_HANDSHAKE_PREFIX); //You configured the path in WebSocketMessageBrokerConfigurer#registerStompEndpoints

        http
                .authorizeRequests()
                .anyRequest().authenticated();
    }

}
Marcus Held
  • 635
  • 4
  • 15
  • Updated my answer. When you don't have a user you're probably missing authentication on the handshake. – Marcus Held Jun 09 '19 at 09:03
  • I updated my question with the piece of code that has the security config as you sugested. Does not work either. – abarazal Jun 10 '19 at 14:57
  • I think your config is wrong, because you call `permitAll()` which disables authentication. – Marcus Held Jun 10 '19 at 16:37
  • So, the principal user name is set by the authentication? – abarazal Jun 10 '19 at 16:44
  • Precisely. You need to do some kind of connection between the user and your request and that's what an authentication is for. So you need to make sure that an authentication takes place when you establish the websocket connection, which is the handshake in your case. – Marcus Held Jun 10 '19 at 16:47
  • But what if I don't want to secure my websocket connection? Spring should have something for this case. – abarazal Jun 10 '19 at 16:49
  • But how should you match a user without knowing if he really is the user he says he is? What you can do is implementing an authentication filter that just "believes" the user by a header information with the username. I wrote a blog article about that a while ago: https://www.code-held.com/2019/05/09/custom-authentication-with-spring-security/ – Marcus Held Jun 10 '19 at 16:58