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;
}