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 { } }