我们的团队有一个应用程序,每天使用 Spring Integration SFTP 从远程服务器获取和处理一次文件。现在,我正在从 Spring Boot 2.7 迁移到 Spring Boot 3。
迁移到 Spring Boot 3 后,我注意到每分钟都会记录以下内容(在 WARN 级别):
2023-08-15T15:00:48.427+02:00 WARN 21256 --- []-nio2-thread-3] i.DefaultSftpClient$SftpChannelSubsystem : handleUnknownChannelRequest(SftpChannelSubsystem[id=0, recipient=0]-ClientSessionImpl[<username>@<host>/<ip>:<port>][sftp]) Unknown channel request: [email protected][want-reply=true]
2023-08-15T15:01:48.430+02:00 WARN 21256 --- [-nio2-thread-13] i.DefaultSftpClient$SftpChannelSubsystem : handleUnknownChannelRequest(SftpChannelSubsystem[id=0, recipient=0]-ClientSessionImpl[<username>@<host>/<ip>:<port>][sftp]) Unknown channel request: [email protected][want-reply=true]
我假设这是从服务器发送到客户端的Keepalive请求。为什么我们的应用程序无法处理该请求?迁移到 Spring Boot 3 之前未显示警告消息。
即使客户端无法处理来自服务器的 keepalive 请求,它仍然会按其应有的方式获取文件。问题是日志文件将充满所有这些不必要的警告消息,并且我们将无法看到其他重要日志。
这是我们用于 SessionFactory 的 Spring Bean
@Bean
public SessionFactory<SftpClient.DirEntry> sftpSessionFactory() {
final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(sftpHost);
factory.setPort(sftpPort);
factory.setUser(sftpUser);
if (sftpPrivateKey != null) {
factory.setPrivateKey(sftpPrivateKey);
factory.setPrivateKeyPassphrase(sftpPrivateKeyPassphrase);
} else {
factory.setPassword(sftpPassword);
}
factory.setAllowUnknownKeys(true);
return new CachingSessionFactory<>(factory);
}
看起来您正面临这种情况:https://issues.apache.org/jira/browse/SSHD-1237.
根据他们的建议,可以注入外部配置的
SshClient
:
SshClient client = SshClient.setupDefaultClient();
List<RequestHandler<ConnectionService>> oldHandlers = client.getGlobalRequestHandlers();
List<RequestHandler<ConnectionService>> handlers = new ArrayList<>();
if (GenericUtils.isNotEmpty(oldHandlers)) {
handlers.add(oldHandlers);
}
handlers.add(KeepAliveHandler.INSTANCE);
client.setGlobalRequestHandlers(handlers);
然后使用这个ctor:
/**
* Instantiate based on the provided {@link SshClient}, e.g. some extension for HTTP/SOCKS.
* @param sshClient the {@link SshClient} instance.
* @param isSharedSession true if the session is to be shared.
*/
public DefaultSftpSessionFactory(SshClient sshClient, boolean isSharedSession) {
遇到了同样的问题,并通过创建一个忽略 Keep Alive 请求的自定义 SFTP 会话工厂来解决它(受到 Artem Bilan 指向
DefaultSftpSessionFactory
类的答案的启发)。
这是自定义 SFTP 会话工厂的示例:
import org.apache.sshd.client.ClientBuilder;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
import org.apache.sshd.client.global.OpenSshHostKeysHandler;
import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
import org.apache.sshd.client.keyverifier.RejectAllServerKeyVerifier;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.common.channel.RequestHandler;
import org.apache.sshd.common.session.ConnectionService;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.integration.sftp.session.ResourceKnownHostsServerKeyVerifier;
import java.util.List;
import static java.util.Arrays.asList;
public class CustomSftpSessionFactory extends DefaultSftpSessionFactory {
private static final List<RequestHandler<ConnectionService>> GLOBAL_REQUEST_HANDLERS = asList(
OpenSshHostKeysHandler.INSTANCE,
CustomOpenSshKeepAliveHandler.INSTANCE
);
public CustomSftpSessionFactory(String password) {
super(setUpDefaultClient(password), false);
}
private static SshClient setUpDefaultClient(String password) {
// Taken from the SshClient.setUpDefaultClient()
final ClientBuilder clientBuilder = ClientBuilder.builder();
clientBuilder.globalRequestHandlers(GLOBAL_REQUEST_HANDLERS);
// Taken from DefaultSftpSessionFactory.doInitInnerClient()
// It would be analogous to the factory.setAllowUnknownKeys()
clientBuilder.serverKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
final SshClient sshClient = clientBuilder.build();
// This is analogous to the factory.setPassword()
sshClient.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(password));
sshClient.setUserInteraction(UserInteraction.NONE);
// You may configure other SSH Client properties, like Private Keys.
return sshClient;
}
}
注意
GLOBAL_REQUEST_HANDLERS
包含 2 个处理程序:
OpenSshHostKeysHandler
时通常提供的默认 SshClient.setUpDefaultClient()
(请参阅 DEFAULT_GLOBAL_REQUEST_HANDLERS
中的 org.apache.sshd.client.ClientBuilder
)。第二个的实现是这样的:
import org.apache.sshd.common.channel.RequestHandler;
import org.apache.sshd.common.global.AbstractOpenSshHostKeysHandler;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
import java.security.PublicKey;
import java.util.Collection;
public class CustomOpenSshKeepAliveHandler extends AbstractOpenSshHostKeysHandler {
public static final CustomOpenSshKeepAliveHandler INSTANCE = new CustomOpenSshKeepAliveHandler();
private static final String REQUEST = "[email protected]";
public CustomOpenSshKeepAliveHandler() {
super(REQUEST);
}
public CustomOpenSshKeepAliveHandler(BufferPublicKeyParser<? extends PublicKey> parser) {
super(REQUEST, parser);
}
protected RequestHandler.Result handleHostKeys(Session session, Collection<? extends PublicKey> keys, boolean wantReply, Buffer buffer) {
// You may want to consider doing something if wantReply is true.
// For my own purposes, answering with Replied was good enough.
return Result.Replied;
}
}
最后,剩下要做的就是使用它。像这样,例如:
final DefaultSftpSessionFactory factory = new CustomSftpSessionFactory(password);
factory.setHost(host);
factory.setPort(port);
factory.setUser(username);