又解锁了一种OpenFeign的使用方式!
在我们日常开发中,相信大家都会接触过对接第三方系统。对接第三方系统最烦人的工作可能就是刚开始对接的时候关于认证、加密、验签、JSON正反序列化等一系列的操作了。
我们知道OpenFeign
它其实是一个http的客户端,主要的应用场景就是在微服务体系内进行微服务之间的相互调用;那么它是不是也可以实现第三方调用?
很明显是可以的!!!
需求分析
在验证我们的观点:OpenFeign
可以实现第三方系统的调用之前,我们先找一个公开的第三方系统协议进行一波简单的需求分析吧。
这里我们使用中电联(中国电力企业联合标准)的协议文档为例。这里附上下载地址,有需要的同学可以自取。
以下为协议文档对于密钥的要求。
通过查看协议文档,我们知道整个对接过程会设计到以下几个需求:
- 调用方式统一使用POST方式
- 传输格式使用JSON
- 传输过程业务数据需要进行加密
- 传输过程整包数据需要生成签名,因为服务端会进行验签,保证数据没有被篡改
- 在进行第三方调用的时候需要像调用其他本地的
Service
一样丝滑(行为一致)
业务实现
为了通过OpenFeign
实现以上需求,我们首先定义一个配置类,用于自定义客户端的配置类。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
@Autowired
private CECOperatorProperties properties;
@Override
public void apply(RequestTemplate requestTemplate) {}
}
- 实现
RequestInterceptor
接口,这里是为了在进行认证拿到access_token
之后,可以通过拦截器在header头放入对应的token信息 - 注入
CECOperatorProperties
属性,对于加解密、验签等操作需要的一些秘钥信息,从配置中心获取后,注入该属性类中 -
@Configuration(proxyBeanMethods = false)
配置该类配置类,并且不会在RootApplicationContext当中注册,只会在使用的时候才会进行相关配置。
这里注意哈,在这个类配置的@Bean
实例,只有在当前的FeignClient
实例的ApplicaitonContext当中可以访问到,其他地方访问不到。具体可以看
接着,我们需要2个基本的数据传输对象:Request
和 Response
@Data
public class CECRequest<T> {
@JsonProperty("OperatorID")
private String operatorID;
@JsonProperty("Data")
private T data;
@JsonProperty("TimeStamp")
private String timeStamp;
@JsonProperty("Seq")
private String seq;
@JsonProperty("Sig")
private String sig;
}
@Data
public class CECResponse<T> {
private Integer Ret;
private T Data;
private String Msg;
}
这里使用@JsonProperty
的原因是协议文档字段的首字母都是大写的,而我们一般的Java字段都是驼峰,为了在进行JSON转换的时候避免无法正常转换。
然后,我们开始自定义编解码器。这里不得不推荐下Hutool 这个类库,是真的强大,因为涉及到的加解密和签名生成,都是现成的。真香!!!
编码器
@Slf4j
public class CECEncoder extends SpringEncoder {
private final CECOperatorProperties properties;
private final HMac mac;
private final AES aes;
public CECEncoder(ObjectFactory<HttpMessageConverters> messageConverters,
CECOperatorProperties properties) {
super(messageConverters);
this.properties = properties;
this.mac = new HMac(HmacAlgorithm.HmacMD5,
properties.getSigSecret().getBytes(StandardCharsets.UTF_8));
this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
properties.getDataSecret().getBytes(),
properties.getDataIv().getBytes());
}
@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
// 数据加密
String data = this.getEncrypt(requestBody);
CECRequest<String> req = new CECRequest<>();
req.setData(data);
req.setSeq("0001");
req.setTimeStamp(DateUtil.formatDate(DateUtil.now(), DateEnum.YYYYMMDDHHMMSS));
req.setOperatorID(properties.getOperatorID());
// 签名计算
String sig = this.getSig(req);
req.setSig(sig.toUpperCase());
super.encode(req, CECRequest.class.getGenericSuperclass(), request);
}
private String getEncrypt(Object requestBody){
String json = JsonUtil.toJson(requestBody);
return Base64.encode(aes.encrypt(json.getBytes()));
}
private String getSig(CECRequest<String> req){
String str = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();
return mac.digestHex(str);
}
}
可以看到,我们的编码器其实是继承了SpringEncoder
,因为在最终编码之后,还是需要转换为JSON发送给服务端,所以在继承SpringEncoder
之后,构造器还需要注入ObjectFactory<HttpMessageConverters>
的实例。另外,在构造器我们也初始化了HMac
和AES
两个实例,一个为了生成签名,一个为了加密业务数据。
在encode
方法,我们把传递进来的requestBody
包装了下,先对其进行加密,然后放在CECRequest
实例的data字段内,并且生成对应的签名,最终请求服务端的时候是一个CECRequest
实例的JSON化的结果。
可能有人会疑惑,为什么这里的requestBody
就直接是业务数据了,而不是CECRequest<T>
实例? 想想我们的第5点需求:在进行第三方调用的时候需要像调用其他本地的Service
一样丝滑(行为一致)。为了实现这个需求,我们不会把非业务的参数暴露给业务调用放,而是在编解码的过程中进行处理。
解码器
@Slf4j
public class CECDecoder extends SpringDecoder {
private final AES aes;
public CECDecoder(ObjectFactory<HttpMessageConverters> messageConverters,
CECOperatorProperties properties) {
super(messageConverters);
this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
properties.getDataSecret().getBytes(),
properties.getDataIv().getBytes());
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
CECResponse<String> resp = this.getCECResponse(response);
// TODO 应该做对应的异常判断然后抛出异常
String json = this.aes.decryptStr(resp.getData());
Response newResp = response.toBuilder().body(json, StandardCharsets.UTF_8).build();
return super.decode(newResp, type);
}
private CECResponse<String> getCECResponse(Response response) throws IOException{
try (InputStream inputStream = response.body().asInputStream()) {
String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
TypeReference<CECResponse<String>> reference = new TypeReference<CECResponse<String>>() {};
return JSONUtil.toBean(json, reference.getType(), true);
}
}
}
解码器会比较简单,只需要进行数据的解密即可。所以我们从Response
中拿到对应的JSON字符串,然后通过反序列化拿到CECResponse
实例,接着做对应的异常判断(这里我的代码暂时未实现),然后再做数据的解码,拿到真正的业务数据的JSON字符串,最后通过OpenFeign
提供的toBuilder
方法重新构造一个新的Response
实例交给SpringDecoder
进行下一步的处理。
下一步,我们把编解码器注册到配置类中。完整的配置类信息如下
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
@Autowired
private CECOperatorProperties properties;
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Encoder encoder(){
return new CECEncoder(messageConverters, properties);
}
@Bean
public Decoder decoder(){
return new CECDecoder(messageConverters, properties);
}
@Override
public void apply(RequestTemplate requestTemplate) {
// TODO 添加Token
}
}
完整的配置类会注入从RootApplicationContext中拿到的ObjectFactory<HttpMessageConverters>
实例,另外再多配置了一个日志实例Logger.Level
,用于在debug的时候打印请求的具体日志。
最后,我们来测试下我们的程序是否正常。简单测试用例如下:
@Slf4j
public class CECTest extends BaseTest{
@Autowired
private CECTokenService tokenService;
@Autowired
private CECStationService stationService;
@Autowired
private CECOperatorProperties properties;
@Test
public void test(){
QueryTokenReq req = new QueryTokenReq();
req.setOperatorID(properties.getOperatorID());
req.setOperatorSecret(properties.getOperatorSecret());
QueryTokenResp resp = tokenService.queryToken(req);
log.info("resp: {}", JsonUtil.toJson(resp));
}
}
看到吧,是不是和调用本地的Service
一样丝滑? 只需要构造对应的入参,即可返回对应的出参,无需关心加密、签名等烦人的操作。相关日志如下:
最后
对于使用OpenFeign
来对接第三方系统我发现还是挺简单的,起码比自己手动去写基本的加密、解密、JSON转换、认证等待,你会发现自己写了一坨的代码,代码量可能还比较多,而用这个方式就简单很多
作者:Anyin
链接:https://juejin.cn/post/7079379888411115533
来源:稀土掘金