SpringCloud Feign HTTPS請求
背景
項目需要調用外部接口,外部資金方的接口安全性較高,有如下要求:
- 使用HTTPS調用,
- 對請求報文進行算法簽名,簽名後結果需要添加到請求parameter中
- 生成一個 32 位的隨機字符串 nonce,將 各個請求參數連同 ticket、nonce 兩個參數進行字典序排序,將排序後的所有參數字符串拼接成一個字符串進行 SHA1 編碼
- SHA1 編碼後的 40 位字符串作爲 sign,sign需要作爲參數添加到url中例如url?sign=xxxx
現狀
實現方式有多種,可以編寫統一的HTTP CLIENT POST方法,帶上HTTPS證書,並且請求之前加簽, 需要自定義重試。
偶然間發現在編寫HTTP CLIENT代碼跟Spring Cloud Feign十分相像,代碼還沒Spring寫的精妙,考慮使用Feign直接調用外部接口
代碼
- 通過指定URL即可直接訪問第三方HTTP接口
@FeignClient(value = "WEBANK", url = "https://xxx:443/api/s/", configuration = WebbankConfiguration.class)
public interface FeignSsl {
@PostMapping(value = "/16080")
QueryOrderStatusRespDto queryOrder(@RequestBody WebankOrderQueryDto webankOrderQueryDto);
}
2. 增加SSL證書,需要準備keystore.jks、truststore.jks ,本例將證書直接BASE64 轉義,可不用使用該方式,直接使用文件。 WebbankConfiguration代碼如下:
import feign.Client;
import feign.Logger;
import feign.codec.Encoder;
import feign.jackson.JacksonEncoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sun.misc.BASE64Decoder;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Demo class
*
* @date 2019/5/28 下午2:28
*/
@Configuration
@Slf4j
public class WebbankConfiguration {
private static final String keyStore = "keyStore";//該字段爲keystore.jks BASE64 值
private static final String trustStore = "trustStore";//該字段爲keystore.jks BASE64 值
private static final String password = "12345678";
@Bean
public Client feignClient() {
Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
log.info("feignClient called");
return trustSSLSockets;
}
//增加SSL
public static SSLSocketFactory getSSLSocketFactory() {
//TODO Exception 需要拋出異常
SSLContext sslContext = null;
BASE64Decoder decoder = new BASE64Decoder();
InputStream keyStoreInput = null;
InputStream trustStoreInput = null;
String keyStoreBaseStr = keyStore;
String userIdBaseStr = trustStore;
try {
byte[] keyStoreBytes = decoder.decodeBuffer(keyStoreBaseStr);
byte[] trustStoreBytes = decoder.decodeBuffer(userIdBaseStr);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStoreInput = new ByteArrayInputStream(keyStoreBytes);
keyStore.load(keyStoreInput, password.toCharArray());
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStoreInput = new ByteArrayInputStream(trustStoreBytes);
trustStore.load(trustStoreInput, null);
sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, password.toCharArray())
.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
}
return sslContext.getSocketFactory();
}
//方便查看日誌,可不用編寫
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
/**
* 增加請求參數加簽
* @param webankOpenApiConfig
* @return
*/
@Bean
WebBankRequestFeignInterceptor webBankRequestFeignInterceptor(@Autowired WebankOpenApiConfig webankOpenApiConfig) {
return new WebBankRequestFeignInterceptor(webankOpenApiConfig);
}
}
3. 增加參數驗籤,feign提供了RequestInterceptor,只要實現了該接口就可以加請求發送之前增加請求頭或者對報文增加參數
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* Demo class
*
* @date 2019/5/29 上午11:11
*/
public class WebBankRequestFeignInterceptor implements RequestInterceptor {
private WebankOpenApiConfig webankOpenApiConfig;
public WebBankRequestFeignInterceptor() {
}
public WebBankRequestFeignInterceptor(WebankOpenApiConfig webankOpenApiConfig) {
this.webankOpenApiConfig = webankOpenApiConfig;
}
public WebankOpenApiConfig getWebankOpenApiConfig() {
return webankOpenApiConfig;
}
public void setWebankOpenApiConfig(WebankOpenApiConfig webankOpenApiConfig) {
this.webankOpenApiConfig = webankOpenApiConfig;
}
@Override
public void apply(RequestTemplate template) {
System.out.println("##########template:"+template);
String parameters = "";
try {
//根據報文BODY計算出驗籤規則
//返回結果爲app_id=XXX&nonce=XX&sign=XXXXX&version=1.0.0
parameters = webankOpenApiConfig.parametersFormat(null, new String(template.body()), "1000000");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ChannelException e) {
e.printStackTrace();
}
System.out.println("##########parameters:" + parameters);
//TODO 考慮有請求參數相關內容,解決?號,在請求URL後增加URL參數
template.insert(template.url().length(),"?"+parameters);
}
}
請求參數,省略其他不需要參數
@Data
public class WebankOrderQueryDto implements Serializable{
private static final long serialVersionUID = 1L;
@JsonProperty("MERCHANT_ID")
private String merchantId;
@JsonProperty("REQ_TIME")
private String reqTime;
@JsonProperty("TXN_ID")
private String txnId;
}