Why SimpUserRegistry does not work properly on an EC2 instance

I use SimpUserRegistry to get the user's online account (with getUserCount() ). And it works well on my local machines, but not on AWS EC2 instances (with Amazon Linux and Ubuntu) with just elastic IP and no load balancing.

The problem with EC2 is that some users are never added to the registry when connected, and therefore I get the wrong results.

I have session listeners for SessionConnectedEvent and SessionDisconnectEvent , where I use SimpUserRegistry (autowired) to get the user's presence. If that matters, I also SimpUserRegistry is a messaging controller.

The following is the websocket message broker configuration:

 @Order(Ordered.HIGHEST_PRECEDENCE + 99) @Configuration @EnableWebSocketMessageBroker @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class WebSocketMessageBrokerConfig extends AbstractWebSocketMessageBrokerConfigurer { @NonNull private SecurityChannelInterceptor securityChannelInterceptor; @Override public void configureMessageBroker(MessageBrokerRegistry config) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(1); threadPoolTaskScheduler.setThreadGroupName("cb-heartbeat-"); threadPoolTaskScheduler.initialize(); config.enableSimpleBroker("/queue/", "/topic/") .setTaskScheduler(threadPoolTaskScheduler) .setHeartbeatValue(new long[] {1000, 1000}); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/websocket") .setAllowedOrigins("*") .withSockJS(); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(securityChannelInterceptor); } } 

And below is the channel interceptor used in the configuration class above:

 @Slf4j @Component @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class SecurityChannelInterceptor extends ChannelInterceptorAdapter { @NonNull private SecurityService securityService; @Value("${app.auth.token.header}") private String authTokenHeader; @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); StompCommand command = accessor.getCommand(); if (StompCommand.CONNECT.equals(command)) { List<String> authTokenList = accessor.getNativeHeader(authTokenHeader); if (authTokenList == null || authTokenList.isEmpty()) { throw new AuthenticationFailureException("STOMP " + command + " missing " + this.authTokenHeader + " header!"); } String accessToken = authTokenList.get(0); AppAuth authentication = securityService.authenticate(accessToken); log.info("STOMP {} authenticated. Authentication Token = {}", command, authentication); accessor.setUser(authentication); SecurityContextHolder.getContext().setAuthentication(authentication); Principal principal = accessor.getUser(); if (principal == null) { throw new RuntimeException("StompHeaderAccessor did not set the authenticated User for " + authentication); } } return message; } } 

I also have the following scheduled task, which simply prints usernames every two seconds:

 @Component @Slf4j @AllArgsConstructor(onConstructor = @__(@Autowired)) public class UserRegistryLoggingTask { private SimpUserRegistry simpUserRegistry; @Scheduled(fixedRate = 2000) public void logUsersInUserRegistry() { Set<String> userNames = simpUserRegistry.getUsers().stream().map(u -> u.getName()).collect(Collectors.toSet()); log.info("UserRegistry has {} users with IDs {}", userNames.size(), userNames); } } 

And some usernames never appear even when connected.

SecurityService class implementation -

 @Service @AllArgsConstructor(onConstructor = @__(@Autowired)) public class SecurityService { private UserRepository userRepository; private UserCredentialsRepository userCredentialsRepository; private JwtHelper jwtHelper; public User getUser() { AppAuth auth = (AppAuth) SecurityContextHolder.getContext().getAuthentication(); User user = (User) auth.getUser(); return user; } public AppAuth authenticate(String accessToken) { String username = jwtHelper.tryExtractSubject(accessToken); if (username == null) { throw new AuthenticationFailureException("Invalid access token!"); } User user = userRepository.findByUsername(username); if (user == null) { throw new AuthenticationFailureException("Invalid access token!"); } AppAuth authentication = new AppAuth(user); return authentication; } } 

Update

The following is an example of a SockJS log in a browser -

Valid server response with user-name header:

 >>> CONNECT AccessToken:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkb2cifQ.Wf8AO77LluHEfEv61TIvugEXxOqIXKjsJBO8QMQh-rF7tzf56lBkdpOruqc7UPf_Pmj6-dnHZ5raq2MnMpeG8Q accept-version:1.1,1.0 heart-beat:10000,10000 <<< CONNECTED version:1.1 heart-beat:1000,1000 user-name:5a590e411b96f841cc00027f 

Invalid response from server without user-name header:

 >>> CONNECT AccessToken:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtb3VzZSJ9.wqX5X_CSdHD8_7PZPiSzftGCuPz1ClQU0-F9RHCqOIIkMLzI4rt31_EAaykc8VojK2KGS6DcycWfAdMr2edzYg accept-version:1.1,1.0 heart-beat:10000,10000 <<< CONNECTED version:1.1 heart-beat:1000,1000 

I also verified that SecurityChannelInterceptor authenticates all users, even if the user-name not in the CONNECTED response.

Update

I deployed the application to the hero. And the problem is happening there too.

Update

When a problem occurs, user in SessionConnectEvent is the set set by SecurityChannelInterceptor , but user in SessionConnectedEvent is null .

Update

AppAuth class -

 public class AppAuth implements Authentication { private final User user; private final Collection<GrantedAuthority> authorities; public AppAuth(User user) { this.user = user; this.authorities = Collections.singleton((GrantedAuthority) () -> "USER"); } public User getUser() { return this.user; } @Override public String getName() { return user.getId(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public Object getCredentials() { return null; } @Override public Object getDetails() { return null; } @Override public Object getPrincipal() { return new Principal() { @Override public String getName() { return user.getId(); } }; } @Override public boolean isAuthenticated() { return true; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { } } 
+8
java spring-messaging spring-websocket amazon-ec2 websocket
source share
2 answers

I was able to track down the problem after some debugging by adding a few logger statements to the StompSubProtocolHandler . After finding the reason, it was concluded that the interceptor channel is not the right place for user authentication. At least for my use case.

Below are some code snippets from StompSubProtocolHandler

The handleMessageFromClient method adds the user to the stompAuthentications card and publishes the SessionConnectEvent event -

 public void handleMessageFromClient(WebSocketSession session, WebSocketMessage<?> webSocketMessage, MessageChannel outputChannel) { //... SimpAttributesContextHolder.setAttributesFromMessage(message); boolean sent = outputChannel.send(message); if (sent) { if (isConnect) { Principal user = headerAccessor.getUser(); if (user != null && user != session.getPrincipal()) { this.stompAuthentications.put(session.getId(), user); } } if (this.eventPublisher != null) { if (isConnect) { publishEvent(new SessionConnectEvent(this, message, getUser(session))); } //... 

And handleMessageToClient fetches the user from the stompAuthentications card and publishes a SessionConnectedEvent -

 public void handleMessageToClient(WebSocketSession session, Message<?> message) { //... SimpAttributes simpAttributes = new SimpAttributes(session.getId(), session.getAttributes()); SimpAttributesContextHolder.setAttributes(simpAttributes); Principal user = getUser(session); publishEvent(new SessionConnectedEvent(this, (Message<byte[]>) message, user)); //... 

getUser method used by the above methods -

 private Principal getUser(WebSocketSession session) { Principal user = this.stompAuthentications.get(session.getId()); return user != null ? user : session.getPrincipal(); } 

Now the problem occurs when the handleMessageToClient fragment handleMessageToClient executed before the handleMessageFromClient fragment. In this case, the user is never added to the DefaultSimpUserRegistry , as it only checks the SessionConnectedEvent .

The following is a snippet of the event listener from DefaultSimpUserRegistry -

 public void onApplicationEvent(ApplicationEvent event) { //... else if (event instanceof SessionConnectedEvent) { Principal user = subProtocolEvent.getUser(); if (user == null) { return; } //... 

Decision

The solution is to extend DefaultHandshakeHandler and override the determineUser method, which is based on this answer . But since I use SockJS, this requires the client to send an authentication token as a request parameter. And the reason for request parameter request is discussed here .

0
source share

A few questions that may help you solve the problem.

  • Obviously, authentication not set for some users. Have you noticed any pattern in which the authentication condition is not set? Based on the source code, DefaultSimpUserRegistry and StompSubProtocolHandler confirms that authentication/principle not set for some users. This is why users are not in SimpUserRegistry.

  • Going through this - Spring Authentication and Authorization It assumes that authentication may not be set if there are no GrantedAuthorities in the authentication/principle .

  • Perhaps you can check the creation of your AppAuth object and see if GrantedAuthorities correctly installed for all users.

Hope this helps in solving your problem.

+1
source share

All Articles