问题
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.
Clearly for some of the users the
authentication
is not being set. Did you notice any pattern, in which condition theauthentication
is not set? Going through the source code of DefaultSimpUserRegistry and StompSubProtocolHandler reaffirmsauthentication/principle
not being set for some of the users. That is why users are missing from SimpUserRegistry.Going through this - Websocket Authentication and Authorization in Spring . It suggests, the
authentication
perhaps not set ifGrantedAuthorities
is missing in theauthentication/principle
.You can probably check the creation of your
AppAuth
object and see if theGrantedAuthorities
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