Another way to use OpenFeign is unlocked!

introduction

Hello everyone, this is Anyin.

In the thing about OpenFeign - usage article , I shared with you OpenFeignsome processing and usage methods in certain scenarios, and today Anyin unlocked OpenFeignanother usage scenario, which can only be said to be really fragrant.

In our daily development, I believe that everyone has come into contact with third-party systems. The most annoying work of connecting with third-party systems may be a series of operations such as authentication, encryption, signature verification, JSON forward and reverse serialization, etc. at the beginning of the connection.

We know that OpenFeignit is actually an http client, and the main application scenario is to make mutual calls between microservices in the microservice system; then, can it also implement third-party calls?

Obviously it can! ! !

demand analysis

Before verifying our point of view: before we OpenFeigncan implement a third-party system call , let's find an open third-party system protocol for a simple demand analysis.

Here we use the protocol document of the China Electricity Council (China Electric Power Enterprise Joint Standard) as an example. The download address is attached here, students who need it can pick it up by themselves.

Joint Standard for Chinese Electric Power Enterprises

The following are the requirements for the key in the protocol document.

image.png

By looking at the protocol document, we know that the entire docking process will be designed to meet the following requirements:

  1. The calling method uses the POST method uniformly
  2. The transfer format uses JSON
  3. Business data needs to be encrypted during transmission
  4. During the transmission process, the entire package of data needs to generate a signature, because the server will verify the signature to ensure that the data has not been tampered with
  5. ServiceWhen making third-party calls, it needs to be as smooth as calling other local ones (consistent behavior)

business realization

In order to OpenFeignachieve the above requirements, we first define a configuration class to customize the client's configuration class.

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
    @Autowired
    private CECOperatorProperties properties;
    @Override
    public void apply(RequestTemplate requestTemplate) {}    
}
复制代码
  1. Implement the interface, here is to put the corresponding token information in the header header through the interceptor after the RequestInterceptorauthentication is obtainedaccess_token
  2. 注入CECOperatorProperties属性,对于加解密、验签等操作需要的一些秘钥信息,从配置中心获取后,注入该属性类中
  3. @Configuration(proxyBeanMethods = false) 配置该类配置类,并且不会在RootApplicationContext当中注册,只会在使用的时候才会进行相关配置。

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

关于OpenFeign那点事儿 - 源码篇

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

@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>的实例。另外,在构造器我们也初始化了HMacAES两个实例,一个为了生成签名,一个为了加密业务数据。

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

Some people may wonder, why requestBodyis the business data directly here, not the CECRequest<T>instance? Consider our 5th requirement: making third-party calls needs to be as Servicesmooth (consistent behavior) as other local calls . In order to achieve this requirement, we will not expose non-business parameters to business calls, but process them in the process of encoding and decoding.

decoder

@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);
        }
    }
}
复制代码

The decoder will be relatively simple, and only need to decrypt the data. So Responsewe get the corresponding JSON string from it, then get the CECResponseinstance through deserialization, then make the corresponding exception judgment (my code is not implemented here), and then decode the data to get the real business data The JSON string, and finally reconstruct a new instance through OpenFeignthe provided method for further processing.toBuilderResponseSpringDecoder

Next, we register the codec into the configuration class. The complete configuration class information is as follows

@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
    }
}
复制代码

The complete configuration class will inject the instance obtained from the RootApplicationContext ObjectFactory<HttpMessageConverters>, and configure another log instance Logger.Levelto print the specific log of the request during debugging.

Finally, let's test whether our program is normal. A simple test case is as follows:

@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));
    }
}
复制代码

ServiceSee, isn't it as smooth as calling the local one ? You only need to construct the corresponding input parameters, and you can return the corresponding output parameters, without worrying about annoying operations such as encryption and signature. The relevant logs are as follows:

image.png

finally

I find that it is quite simple to use OpenFeignit to connect with third-party systems. At least it is better than manually writing basic encryption, decryption, JSON conversion, and authentication. And it's much simpler this way.

Last HutoolYYDS!!!

Guess you like

Origin juejin.im/post/7079379888411115533