又解锁了一种OpenFeign的使用方式!
创始人
2024-02-17 16:43:31
0

引言

Hello 大家好,这里是Anyin。

在关于OpenFeign那点事儿 - 使用篇 中和大家分享了关于OpenFeign在某些场景下的一些处理和使用方法,而今天Anyin再次解锁了OpenFeign的又一个使用场景,只能说真香。

在我们日常开发中,相信大家都会接触过对接第三方系统。对接第三方系统最烦人的工作可能就是刚开始对接的时候关于认证、加密、验签、JSON正反序列化等一系列的操作了。

我们知道OpenFeign它其实是一个http的客户端,主要的应用场景就是在微服务体系内进行微服务之间的相互调用;那么它是不是也可以实现第三方调用?

很明显是可以的!!!

需求分析

在验证我们的观点:OpenFeign可以实现第三方系统的调用之前,我们先找一个公开的第三方系统协议进行一波简单的需求分析吧。

这里我们使用中电联(中国电力企业联合标准)的协议文档为例。这里附上下载地址,有需要的同学可以自取。

中国电力企业联合标准

以下为协议文档对于密钥的要求。

通过查看协议文档,我们知道整个对接过程会设计到以下几个需求:

  1. 调用方式统一使用POST方式
  2. 传输格式使用JSON
  3. 传输过程业务数据需要进行加密
  4. 传输过程整包数据需要生成签名,因为服务端会进行验签,保证数据没有被篡改
  5. 在进行第三方调用的时候需要像调用其他本地的Service一样丝滑(行为一致)

业务实现

为了通过OpenFeign实现以上需求,我们首先定义一个配置类,用于自定义客户端的配置类。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {@Autowiredprivate CECOperatorProperties properties;@Overridepublic void apply(RequestTemplate requestTemplate) {}    
}
复制代码
  1. 实现RequestInterceptor接口,这里是为了在进行认证拿到access_token之后,可以通过拦截器在header头放入对应的token信息
  2. 注入CECOperatorProperties属性,对于加解密、验签等操作需要的一些秘钥信息,从配置中心获取后,注入该属性类中
  3. @Configuration(proxyBeanMethods = false) 配置该类配置类,并且不会在RootApplicationContext当中注册,只会在使用的时候才会进行相关配置。

这里注意哈,在这个类配置的@Bean实例,只有在当前的FeignClient实例的ApplicaitonContext当中可以访问到,其他地方访问不到。具体可以看

关于OpenFeign那点事儿 - 源码篇

接着,我们需要2个基本的数据传输对象:Request 和 Response

@Data
public class CECRequest {@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 {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 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());}@Overridepublic void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {// 数据加密String data = this.getEncrypt(requestBody);CECRequest 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 req){String str = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();return mac.digestHex(str);}
}
复制代码

可以看到,我们的编码器其实是继承了SpringEncoder,因为在最终编码之后,还是需要转换为JSON发送给服务端,所以在继承SpringEncoder之后,构造器还需要注入ObjectFactory的实例。另外,在构造器我们也初始化了HMacAES两个实例,一个为了生成签名,一个为了加密业务数据。

encode方法,我们把传递进来的requestBody包装了下,先对其进行加密,然后放在CECRequest实例的data字段内,并且生成对应的签名,最终请求服务端的时候是一个CECRequest实例的JSON化的结果。

可能有人会疑惑,为什么这里的requestBody就直接是业务数据了,而不是CECRequest实例? 想想我们的第5点需求:在进行第三方调用的时候需要像调用其他本地的Service一样丝滑(行为一致)。为了实现这个需求,我们不会把非业务的参数暴露给业务调用放,而是在编解码的过程中进行处理。

解码器

@Slf4j
public class CECDecoder extends SpringDecoder {private final AES aes;public CECDecoder(ObjectFactory messageConverters,CECOperatorProperties properties) {super(messageConverters);this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,properties.getDataSecret().getBytes(),properties.getDataIv().getBytes());}@Overridepublic Object decode(Response response, Type type) throws IOException, FeignException {CECResponse 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 getCECResponse(Response response) throws IOException{try (InputStream inputStream = response.body().asInputStream()) {String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);TypeReference> reference = new TypeReference>() {};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 {@Autowiredprivate CECOperatorProperties properties;@Autowiredprivate ObjectFactory messageConverters;@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}@Beanpublic Encoder encoder(){return new CECEncoder(messageConverters, properties);}@Beanpublic Decoder decoder(){return new CECDecoder(messageConverters, properties);}@Overridepublic void apply(RequestTemplate requestTemplate) {// TODO 添加Token}
}
复制代码

完整的配置类会注入从RootApplicationContext中拿到的ObjectFactory实例,另外再多配置了一个日志实例Logger.Level,用于在debug的时候打印请求的具体日志。

最后,我们来测试下我们的程序是否正常。简单测试用例如下:

@Slf4j
public class CECTest extends BaseTest{@Autowiredprivate CECTokenService tokenService;@Autowiredprivate CECStationService stationService;@Autowiredprivate CECOperatorProperties properties;@Testpublic 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一样丝滑? 只需要构造对应的入参,即可返回对应的出参,无需关心加密、签名等烦人的操作。相关日志如下:

 

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...