Search code examples
springendpointhmacsignedfeign

Feign Client Signed Endpoint


I am using Spring Feign Client to access Binance API.

Certain APIs such as SIGNED Endpoint Examples for POST /api/v3/order require to be signed using -sha256 -hmac.

The documentation tells how do call the signed API using cURL + OpenSSL

Example 1: As a request body

requestBody:

symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559

HMAC SHA256 signature:

[linux]$ echo -n "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559" | openssl dgst -sha256 -hmac "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"
(stdin)= c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71

curl command:

(HMAC SHA256)
[linux]$ curl -H "X-MBX-APIKEY: vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A" -X POST 'https://api.binance.com/api/v3/order' -d 'symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559&signature=c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71'

How can I do it using FeignClient?

Do I have to create a RequestInterceptor?

Any advice would be greatly appreciated.

Regards,

Flávio Oliva


Solution

  • This is my final solution:

    I am using spring-boot 2.3.3.

    @FeignClient(name = "order", url = "${binance.api.url}", decode404 = true, configuration = SignedEndpointFeignConfiguration.class)
    public interface OrderApi {
    
    
        @PostMapping(value = "/api/v3/order", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
        ResponseEntity<String> newOrder(@SpringQueryMap OrderRequest orderRequest);
    
    }
    
    @Slf4j
    public class SignedEndpointFeignConfiguration extends BinanceDefaultFeignConfiguration {
    
        public SignedEndpointFeignConfiguration(ApplicationProperties.BinanceApi binanceApi) {
            super(binanceApi);
        }
    
        @Bean
        public RequestInterceptor requestInterceptor() {
            return new SignatureInterceptor(binanceApi);
        }
    
    }
    
    @Slf4j
    public class BinanceDefaultFeignConfiguration {
    
        protected final ApplicationProperties.BinanceApi binanceApi;
    
        public BinanceDefaultFeignConfiguration(ApplicationProperties.BinanceApi binanceApi) {
            this.binanceApi = binanceApi;
        }
    
        @Bean
        public ErrorDecoder errorDecoder() {
            return new FeignErrorDecoder();
        }
    
        @Bean
        public Logger.Level logger() {
            return Logger.Level.FULL;
        }
    
        @Bean
        public Encoder encoder() {
            return new JacksonEncoder();
        }
    
        @Bean
        public Decoder decoder() {
            return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter()));
        }
    
        public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
            final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new GateWayMappingJackson2HttpMessageConverter());
            return () -> httpMessageConverters;
        }
    
        public static class GateWayMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
            GateWayMappingJackson2HttpMessageConverter() {
                List<MediaType> mediaTypes = new ArrayList<>();
                mediaTypes.add(MediaType.APPLICATION_JSON);
                setSupportedMediaTypes(mediaTypes);
            }
        }
    
        @Bean
        public RequestInterceptor requestInterceptor() {
            return (RequestTemplate template) -> template.header("X-MBX-APIKEY", binanceApi.apiKey);
        }
    }
    
    @Slf4j
    @AllArgsConstructor
    public class SignatureInterceptor implements RequestInterceptor {
    
        protected final ApplicationProperties.BinanceApi binanceApi;
    
        @Override
        public void apply(RequestTemplate template) {
            addApiKeyToHeader(template);
            addSignatureToQueryParams(template);
        }
    
        private void addApiKeyToHeader(RequestTemplate template) {
            template.header("X-MBX-APIKEY", binanceApi.apiKey);
        }
    
        private void addSignatureToQueryParams(RequestTemplate template) {
            final String signature = Signature.encode(binanceApi.secretKey, getQueryLineWithoutQuestionMark(template));
            log.debug("Signature: {}", signature);
            template.query("signature", signature);
        }
    
    
        private static String getQueryLineWithoutQuestionMark(RequestTemplate template) {
            final String queryLineWithoutQuestionMark = template.queryLine().substring(1);
            log.debug("Request Params: {}", queryLineWithoutQuestionMark);
            return template.queryLine().substring(1);
        }
    
    }
    
    import org.apache.commons.codec.binary.Hex;
    
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    
    /**
     * Utility class used to sign a provided data.
     */
    public class Signature {
        /**
         * @param key the key used to sign the data.
         * @param data the data to be signed in UTF-8 format.
         * @return the data signature.
         */
        public static String encode(String key, String data) {
            try {
                Mac hmac = Mac.getInstance("HmacSHA256");
                SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                hmac.init(secret_key);
                return new String(Hex.encodeHex(hmac.doFinal(data.getBytes(StandardCharsets.UTF_8))));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    @Component
    @PropertySource(value = "classpath:/application.yml")
    public class ApplicationProperties {
    
        @Component
        @ConfigurationProperties(value = "binance.api")
        public static class BinanceApi {
    
            @Value("${url}")
            public String url;
    
            @Value("${apiKey}")
            public String apiKey;
    
            @Value("${secretKey}")
            public String secretKey;
    
        }
    
    }
    
    binance:
      api:
        url: https://api.binance.com
        apiKey: abc
        secretKey: xyz
    
    
    logging:
      level:
        org.springframework: INFO