Why is SimpUserRegistry not working properly on EC2 Instance

拈花ヽ惹草 提交于 2019-12-23 08:54:06

问题


I am using SimpUserRegistry to get online user-count (with getUserCount()). And it is working good on my local machines but not on AWS EC2 instances (tried with Amazon Linux and Ubuntu) with just elastic IP and no load balancer.

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

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

Below is the websocket message broker config:

@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 above config class:

@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 following scheduled task which simply prints the user names 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 user names never show up even when connected.

The implementation of SecurityService class -

@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

Following is an example of SockJS logs on browser -

Correct response from server 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

Incorrect response from server with no 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 have also verified that the SecurityChannelInterceptor is authenticating all the users, even when the user-name is not in the CONNECTED response.

Update

I deployed the app on heroku. And the issue is happening there as well.

Update

When issue occurs, user in SessionConnectEvent is the one 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 {

    }
}

回答1:


Just a few points that may help you in resolving the problem.

  1. Clearly for some of the users the authentication is not being set. Did you notice any pattern, in which condition the authentication is not set? Going through the source code of DefaultSimpUserRegistry and StompSubProtocolHandler reaffirms authentication/principle not being set for some of the users. That is why users are missing from SimpUserRegistry.

  2. Going through this - Websocket Authentication and Authorization in Spring . It suggests, the authentication perhaps not set if GrantedAuthorities is missing in the authentication/principle.

  3. You can probably check the creation of your AppAuth object and see if the GrantedAuthorities is properly set for all the users.

Hope this may help someway in resolving your issue.




回答2:


I was able to track the issue after some debugging by adding a few logger statements in the StompSubProtocolHandler. After finding the cause, conclusion was that a channel-interceptor is not a correct place to authenticate an user. At least for my use-case.

Following are some of the code snippets from StompSubProtocolHandler -

The handleMessageFromClient method adds the user to the stompAuthentications map and publishes a 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 the handleMessageToClient retrieves the user from the stompAuthentications map 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 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 snippet executes before the handleMessageFromClient snippet. In this case, user is never added to the DefaultSimpUserRegistry, as it only checks the SessionConnectedEvent.

Below is the event listener snippet from DefaultSimpUserRegistry -

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

Solution

The solution is to extend DefaultHandshakeHandler and override determineUser method, which is based on this answer. But, as I am using SockJS, this requires the client to send auth-token as a query parameter. And the reason for the query parameter requirement is discussed here.



来源:https://stackoverflow.com/questions/48246245/why-is-simpuserregistry-not-working-properly-on-ec2-instance

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!