Spring - Повторить запрос, если служба возвращает 409 HTTP-код

У меня есть приложение Spring + CXF, которое использует API передачи: Передача RPC работает на другом сервере.

В соответствии с документами передачи вам необходимо отправить токен, который генерируется по первому запросу. Затем сервер отвечает 409 http-кодом вместе с заголовком, содержащим токен. Этот токен должен быть отправлен на все последующие вызовы:

2.3.1. Защита CSRF. Большинство серверов RPC с передачей требуют, чтобы заголовок X-Transmission-Session-Id отправлялся с запросами, чтобы предотвратить Атаки CSRF. Когда ваш запрос имеет неправильный идентификатор - например, когда вы отправьте свой первый запрос или когда на сервере истекает токен CSRF - сервер RPC передачи вернет ошибку HTTP 409 с правой X-Transmission-Session-Id в своих собственных заголовках. Итак, правильный способ обработки ответа 409 - это обновить X-Transmission-Session-Id и повторно отправить предыдущий запрос.

Я искал решение, используя фильтр CXF или перехватчик, который в основном будет обрабатывать ответ 409 и повторить первоначальный запрос, добавляя заголовок маркера. Я думаю, что клиенты могут сохранить этот токен и отправить его в будущих вызовах.

Я не очень хорошо знаком с cxf, поэтому мне было интересно, можно ли это сделать и как. Любые подсказки были бы полезны.

Спасибо!

Ответ 1

Здесь spring-retry, который теперь является независимым проектом и больше не является частью spring -batch.

Как объяснено здесь повторный вызов повторного запроса поможет сделать другой вызов обновленным с заголовком токена.

Псевдокод/​​логика в этом случае будет выглядеть примерно как

RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        /* 
         * 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
         * 2. Call the transmission API 
         * 3.a. If API responds with 409, read the token 
         *    3.a.1. Store the token in RetryContext via setAttribute method
         *    3.a.2. Throw a custom exception so that retry kicks in
         * 3.b. If API response is non 409 handle according to business logic
         * 4. Return result
         */
    }
});

Обязательно настройте RetryTemplate с разумными политиками повтора и отсрочки, чтобы избежать конфликтов/сюрпризов ресурсов.

Знайте в комментариях в случае любых запросов /roadblock.

NB: RetryContext реализация RetryContextSupport имеет метод hasAttribute и setAttribute, унаследованный от Spring core AttributeAccessor

Ответ 2

Предполагая, что вы используете Apache CXF JAX RS Client, это легко сделать, просто создав пользовательское исключение времени выполнения и ResponseExceptionMapper для него, Таким образом, идея состоит в том, чтобы вручную преобразовать 409 результатов в какое-то исключение, а затем обработать их правильно (в вашем случае повторите вызов службы).

См. следующий код, снятый для полного рабочего примера.

@SpringBootApplication
@EnableJaxRsProxyClient
public class SpringBootClientApplication {
    // This can e stored somewhere in db or elsewhere 
    private static String lastToken = "";

    public static void main(String[] args) {
        SpringApplication.run(SpringBootClientApplication.class, args);
    }

    @Bean
    CommandLineRunner initWebClientRunner(final TransmissionService service) {
        return new CommandLineRunner() {
            @Override
            public void run(String... runArgs) throws Exception {
                try {
                    System.out.println(service.sayHello(1, lastToken));
                // catch the TokenExpiredException get the new token and retry
                } catch (TokenExpiredException ex) {
                    lastToken = ex.getNewToken();
                    System.out.println(service.sayHello(1, lastToken));
                }
             }
        };
    }

    public static class TokenExpiredException extends RuntimeException {
        private String newToken;

        public TokenExpiredException(String token) {
            newToken = token;
        }

        public String getNewToken() {
            return newToken;
        }
     }

     /**
      * This is where the magic is done !!!!
     */
     @Provider
     public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {

        @Override
        public TokenExpiredException fromResponse(Response r) {
            if (r.getStatus() == 409) {
                return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
            }
            return null;
        }

    }

    @Path("/post")
    public interface TransmissionService {
        @GET
        @Path("/{a}")
        @Produces(MediaType.APPLICATION_JSON_VALUE)
        String sayHello(@PathParam("a") Integer a, @HeaderParam("X-Transmission-Session-Id") String sessionId)
            throws TokenExpiredException;
    }
}