Java 在Spring AMQP中检测凭据删除并从中恢复
我们有一个Spring云配置设置,使用Vault数据库后端(MySQL和RabbitMQ),这使我们能够将生成的凭据注入到属性中,例如:Java 在Spring AMQP中检测凭据删除并从中恢复,java,spring-boot,spring-amqp,spring-cloud-config,Java,Spring Boot,Spring Amqp,Spring Cloud Config,我们有一个Spring云配置设置,使用Vault数据库后端(MySQL和RabbitMQ),这使我们能够将生成的凭据注入到属性中,例如: spring.rabbitmq.username spring.rabbitmq.password 当我们的应用程序启动时,我们有一组新的兔子凭证,并且我们能够根据需要请求一组新的凭证 由于我们的Rabbit凭据由Vault进行外部管理,因此它们可能在应用程序生命周期内的任何时间过期/删除(这也是一个弹性测试场景) 我的问题是我们如何(有效、可靠地):
spring.rabbitmq.username
spring.rabbitmq.password
- 检测生成的凭据是否过期
- 使用新凭据更新现有的Spring AMQP
CachingConnectionFactory
CachingConnectionFactory
可能性包括:
ChannelListener
,它构建了所有新创建的
频道
s,并尝试在每个频道上创建/删除匿名队列
每x秒,通过ShutdownListener
监听任何shutdownlessignalException
s
可能有403状态代码。这似乎是可行的,但有点复杂,我们已经看到并发性问题在shutdown处理程序中起到了非常重要的作用CachingConnectionFactory
。我们尝试使用该类的一个克隆,但除了其复杂性之外,我们最终在创建队列时出现了RESOURCE\u LOCKED
错误访问被拒绝
——当CachingConnectionFactory
尝试使用已删除的凭据时得到的结果——通常被视为致命的错误配置错误,而不是任何实际工作流程的一部分,或者可以从中恢复
这里有一个优雅的解决方案吗
使用:Spring Boot 1.5.10-RELEASE,Spring Cloud Dalston SR4
更新:
在
RabbitTemplate
端,即使CachingConnectionFactory
正确检测到对我发送到的交换机的访问被拒绝
,也不会引发异常(无论是否使用RetryTemplate
)
配置为:
spring
rabbitmq:
host: rabbitmq.service.consul
port: 5672
virtualHost: /
template:
retry:
enabled: true
代码是:
@Autowired private RabbitTemplate rt; // From RabbitAutoConfiguration
@Bean
public DirectExchange emailExchange() {
return new DirectExchange("email");
}
public void sendEmail() {
this.rt.send("email", "email.send", "test payload");
}
应用程序启动,声明电子邮件
交换。RabbitMQ UI显示我的(生成的)用户和到exchange的连接,这在启动时是正常的。然后,我通过在运行本地测试调用上面的sendmail()
email之前使用UI手动删除该用户来模拟凭据过期
rabbitmplate
调用不会引发异常或记录错误,但会记录以下(预期)错误:
[AMQP连接127.0.0.1:5672]错误
o、 s.a.r.c.CachingConnectionFactory-通道关闭:通道错误;
协议方法:#方法(回复代码=403,
回复文本=访问被拒绝-访问vhost“/”中的exchange“电子邮件”
为用户拒绝
“cert-configserver-75c3ae60-da76-3058-e7df-a7b90ef72171”,类别id=60,
方法id=40)
除了在所有
rabbitmplate.send()
调用之前检查凭据之外,我想知道是否有任何方法可以捕获发送过程中出现的访问被拒绝
错误,以便我可以像对侦听器一样刷新凭据,并给RetryTemplate
重试的机会。对于这种情况,侦听器容器会发出ListenerContainerConsumerFailedEvent
。您可以听一听这个,检查它的原因
和异常,然后决定停止()
容器并执行您需要的其他操作。然后start()
在rabbitmplate
端,只需要try…catch
调用并分析异常,原因与此相同
到目前为止,这不是我尝试过的,但这是我对如何处理访问被拒绝
状态的最好感觉。从CachingConnectionFactory
的角度来看,你真的不能在这件事上做任何事情
更新
我的申请如下:
spring.rabbitmq.username=test
spring.rabbitmq.password=test
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1ms
logging.level.org.springframework.retry=DEBUG
这就是我为那个不存在的用户运行此应用程序时在控制台中的内容:
Error during sending: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.
更新2
我的发现也是因为我们可以制作这些道具:
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.template.mandatory=true
然后添加一个rabbitmplate.setConfirmCallback()
,我们被拒绝的异步发送消息将被拒绝。但是,它仍然是一个异步回调,类似于前面提到的ChannelListener
。从Spring AMQP的角度来看,真的没有什么可做的。所有内容都是AMQP协议的异步特性,可能真的需要一些来自兔子客户端库的“快速失败”挂钩
请在rabbitmq用户
Google组上问这样一个问题。那是拉比特人常去的地方
更新3
作为代理上此类事件的解决方案,可以使用。特定的user.deleted
或user.password.changed
事件由代理发出 经过大量的实验和调试,我采用了
所以现在,我不再试图跟踪Spring和Rabbit代码中的ShutdowsSignalException
和ListenerContainerConsumerFailedEvent
事件,而是在SimpleMessageListenerContainer
和RabbitTemplate
之间进行跟踪
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.template.mandatory=true
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import static org.springframework.amqp.core.ExchangeTypes.TOPIC;
@Component
public class ReuathenticationListener {
private static Logger log = LoggerFactory.getLogger(ReuathenticationListener.class);
@Autowired private RabbitProperties rabbitProperties;
@Autowired private RefreshEndpoint refreshEndpoint;
@Autowired private CachingConnectionFactory connectionFactory;
@RabbitListener(
id = "credential_expiry_listener",
bindings = @QueueBinding(value = @Queue(value="credentials.expiry", autoDelete="true", durable="false"),
exchange = @Exchange(value="amq.rabbitmq.event", type=TOPIC, internal="true", durable="true"),
key = "user.#")
)
public void expiryHandler(final MessageHeaders headers) {
final String key = (String) headers.get("amqp_receivedRoutingKey");
// See: https://www.rabbitmq.com/event-exchange.html
if (!key.equals("user.deleted") &&
!key.equals("user.authentication.failure")) {
return;
}
final String failedName = (String) headers.get("name");
final String prevUsername = rabbitProperties.getUsername();
if (!failedName.equals(prevUsername)) {
log.debug("Ignore expiry of unrelated user: " + failedName);
return;
}
log.info("Refreshing Rabbit credentials...");
refreshEndpoint.refresh();
log.info("Refreshed username: '" + prevUsername + "' => '" + rabbitProperties.getUsername() + "'");
connectionFactory.setUsername(rabbitProperties.getUsername());
connectionFactory.setPassword(rabbitProperties.getPassword());
connectionFactory.resetConnection();
log.info("CachingConnectionFactory reset, reconnection should now begin.");
}
}