我正在使用SimpUserRegistry
来获取在线用户计数(使用getUserCount()
)。它在我的本地机器上运行良好,但在AWS EC2实例(使用Amazon Linux和Ubuntu试用)上只有弹性IP和无负载均衡器。
EC2上的问题是某些用户在连接时从未添加到注册表中,因此我得到错误的结果。
我有SessionConnectedEvent
和SessionDisconnectEvent
的会话侦听器,我使用SimpUserRegistry
(自动装配)来获取用户。如果重要的话,我SimpUserRegistry
也是一个消息传递控制器。
下面是websocket消息代理配置:
@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);
}
}
以下是上面配置类中使用的通道拦截器:
@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;
}
}
我还有以下计划任务,它只是每两秒打印一次用户名:
@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);
}
}
有些用户名即使在连接时也不会显示。
SecurityService
类的实现 -
@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;
}
}
更新
以下是浏览器上的SockJS日志示例 -
使用user-name
标题来纠正来自服务器的响应:
>>> 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
来自没有user-name
标题的服务器的错误响应:
>>> 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
我还验证了SecurityChannelInterceptor
正在验证所有用户,即使user-name
不在CONNECTED
响应中也是如此。
更新
我在heroku上部署了应用程序。问题也在那里发生。
更新
发生问题时,user
中的SessionConnectEvent
是由SecurityChannelInterceptor
设置的user
,但SessionConnectedEvent
中的null
为AppAuth
。
更新
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 {
}
}
课程 -
Cache-Control
答案 0 :(得分:1)
只有几点可以帮助您解决问题。
显然,对于某些用户,authentication
尚未设置。您是否注意到任何模式,在哪种情况下authentication
未设置?浏览DefaultSimpUserRegistry和StompSubProtocolHandler的源代码,重申authentication/principle
没有为某些用户设置。这就是SimpUserRegistry缺少用户的原因。
完成此操作 - Websocket Authentication and Authorization in Spring。它表明,如果authentication
中缺少GrantedAuthorities
,authentication/principle
可能未设置。
您可以检查AppAuth
对象的创建情况,看看是否为所有用户正确设置了GrantedAuthorities
。
希望这可能有助于解决您的问题。
答案 1 :(得分:1)
通过在StompSubProtocolHandler
中添加一些记录器语句,我能够在调试后跟踪问题。
找到原因后,结论是通道拦截器不是验证用户身份的正确位置。至少对我的用例来说。
handleMessageFromClient
方法将用户添加到stompAuthentications
地图并发布SessionConnectEvent
事件 -
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)));
}
//...
handleMessageToClient
从stompAuthentications
地图检索用户并发布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
方法 -
private Principal getUser(WebSocketSession session) {
Principal user = this.stompAuthentications.get(session.getId());
return user != null ? user : session.getPrincipal();
}
现在,handleMessageToClient
代码段在handleMessageFromClient
代码段之前执行时会出现问题。在这种情况下,用户永远不会添加到DefaultSimpUserRegistry
,因为它只会检查SessionConnectedEvent
。
以下是来自DefaultHandshakeHandler
-
public void onApplicationEvent(ApplicationEvent event) {
//...
else if (event instanceof SessionConnectedEvent) {
Principal user = subProtocolEvent.getUser();
if (user == null) {
return;
}
//...
<强>解决方案强>
解决方案是扩展this answer并覆盖determineUser
方法,该方法基于here。但是,当我使用SockJS时,这需要客户端发送auth-token作为查询参数。并且讨论了查询参数要求的原因model finder。