feign的加解密封装

  • Post author:
  • Post category:其他




功能描述

通过覆盖 feign.codec.Encoder 和 feign.codec.Decoder

实现 feign 请求的加解密操作

采用动态的 feignClient 调用,平台统一的通信加解密策略

同一个服务节点可以同时使用非加密的 customFeign 和 使用我方平台加密的 partnerFeign



1. 前言

我这边是支付渠道,调用第三方支付的callback请求

自我感觉良好,分享给同学们



2. 核心代码



2.1 FeignRequestEncoder 请求加密

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.mea.pay.api.infrastructure.IFeignEncoder;
import com.mea.pay.common.constants.ConstantHttp;
import com.mea.pay.common.util.AESEncryptUtil;
import com.mea.pay.common.util.CommonUtil;
import com.mea.pay.common.util.RSAUtil;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.lang.reflect.Type;

import static com.mea.pay.notifycenter.config.SandboxNotifyConfig.PRIVATE_KEY;

/**
 * 覆盖 feign 的 加密操作
 *
 * @author Heng.Wei
 * @date 2022/4/20 9:44
 **/
@Slf4j
@Primary
@Component
public class FeignRequestEncoder implements IFeignEncoder {

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {

        if(!String.class.equals(bodyType)){
            return;
        }
        // 我们和外围厂商交互默认是 application/json 格式,下面对 requestbody 做加密操作
        String requestBodyString = object.toString();
        // 随机生成的16位AES密钥
        String aesKey = CommonUtil.getValue();
        String encryptedAesKey;
        try {
            // 私钥加密AES密钥
            encryptedAesKey = RSAUtil.encryptByPrivateKey(PRIVATE_KEY, aesKey);
        } catch (Exception e) {
            log.error("meapay feign encode exception, message:{}, detail:{}", e.getMessage(), JSON.toJSONString(e));
            throw new EncodeException("meapay feign encode exception:" + e.getMessage());
        }
        // 对 requestBody 做AES对称加密
        String data = AESEncryptUtil.encryptBase64(requestBodyString, aesKey);
        // 组装 requestBody
        JSONObject requestBody = new JSONObject();
        requestBody.put(ConstantHttp.CODE, encryptedAesKey);
        requestBody.put(ConstantHttp.DATA, data);
        template.body(requestBody.toJSONString());
    }
}



2.2 响应解密

FeignResponseDecoder 响应解密

package com.mea.pay.notifycenter.config;

import com.alibaba.fastjson.JSON;
import com.mea.pay.api.infrastructure.IFeignDecoder;
import com.mea.pay.common.exception.BusinessException;
import com.mea.pay.common.util.AESEncryptUtil;
import com.mea.pay.common.util.RSAUtil;
import com.mea.pay.notifycenter.domain.dto.FeignResponseDTO;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.Type;

import static com.mea.pay.notifycenter.config.SandboxNotifyConfig.PRIVATE_KEY;
import static java.lang.String.format;

/**
 * 覆盖 feign 的 Decoder 实现类,实现解密操作
 * @author Heng.Wei
 * @date 2022/4/19 18:15
 **/
@Slf4j
@Primary
@Component
public class FeignResponseDecoder implements IFeignDecoder {

    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {

        Response.Body body = response.body();
        if (response.status() == HttpStatus.NOT_FOUND.value() || response.status() == HttpStatus.NO_CONTENT.value()){
            return Util.emptyValueOf(type);
        }
        if (body == null){
            return null;
        }
        if (byte[].class.equals(type)) {
            return Util.toByteArray(body.asInputStream());
        }
        if (String.class.equals(type)) {
            String bodyString = Util.toString(body.asReader(Util.UTF_8));
            // 解密
            return decryptResponse(bodyString);
        }
        throw new DecodeException(response.status(),
                format("%s is not a type supported by this decoder.", type), response.request());
    }

    /**
     * 解密响应体
     *
     * @param encodedResponse 加密的响应内容
     * @return java.lang.String
     * @author Heng.Wei
     * @date 2022/4/18 18:20
     **/
    public String decryptResponse(String encodedResponse){

        FeignResponseDTO feignResponseDTO = JSON.parseObject(encodedResponse, FeignResponseDTO.class);
        String body;
        try {
            // 私钥对 secretKey 解密,得到AES KEY
            String aesKey = RSAUtil.decryptByPrivateKey(PRIVATE_KEY, feignResponseDTO.getSecretCode());
            body = AESEncryptUtil.decryptBase64(feignResponseDTO.getEncryptedData(), aesKey);
        } catch (Exception e) {
            log.error("feignResponse解密异常, 提示:{}, 异常:{}", e.getMessage(), JSON.toJSONString(e));
            throw new BusinessException("feignResponse decode exception:" + e.getMessage());
        }
        return body;
    }
}



3. 测试结果示例



feign的加解密示例

feign请求 – 加密前示例

在这里插入图片描述

FeignRequestEncoder 加密后示例

在这里插入图片描述

FeignResponseDecoder 解密前示例

在这里插入图片描述

FeignResponseDecoder 解密后示例

在这里插入图片描述



4. 其他辅助类提供 – 仅供参考

CustomFeign

import feign.HeaderMap;
import feign.RequestLine;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;

import java.net.URI;
import java.util.Map;


@FeignClient(value = "custom-feign")
public interface CustomFeign {

    @RequestLine("POST")
    String postRequest(URI baseUri, @HeaderMap Map<String, Object> headerMap, @RequestBody String request);

    @RequestLine("GET")
    String getRequest(URI baseUri, @HeaderMap Map<String, Object> headerMap, @RequestBody String request);

}

FeignClientEncryptedServiceImpl 平台统一加解密的 feignService

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;


/**
 * 动态feignClient - feign的加密请求
 * @author Heng.Wei
 * @date 2022/4/19 14:50
 **/
@Slf4j
@Component("feignClientEncryptedService")
@ConditionalOnBean({IFeignDecoder.class, IFeignEncoder.class})
public class FeignClientEncryptedServiceImpl implements IFeignClientService{

    private final CustomFeign partnerFeign;

    @Autowired
    public FeignClientEncryptedServiceImpl(@Qualifier("partnerFeign") CustomFeign partnerFeign) {
        this.partnerFeign = partnerFeign;
    }

    /**
     * POST请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    @Override
    public String postRequest(String url, Map<String, Object> header, String content) {
        try {
            return partnerFeign.postRequest(new URI(url), header, content);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            log.error("远程调用异常:{}", e.getMessage());
            throw new RuntimeException("postExecute exception:" + e.getMessage());
        }
    }


    /**
     * GET请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    @Override
    public String getRequest(String url, Map<String, Object> header, String content) {
        try {
            return partnerFeign.getRequest(new URI(url), header, content);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            log.error("远程调用异常:{}", e.getMessage());
            throw new RuntimeException("postExecute exception:" + e.getMessage());
        }
    }
}

FeignClientServiceImpl 通用的 feign service – 未作加解密操作的正常通信

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;


/**
 * 动态feignClient - feign的常规请求
 * @author Heng.Wei
 * @date 2022/4/19 14:58
 **/
@Slf4j
@Primary
@Component("feignClientService")
public class FeignClientServiceImpl implements IFeignClientService{

    private final CustomFeign customFeign;

    @Autowired
    public FeignClientServiceImpl(@Qualifier("customFeign") CustomFeign customFeign) {
        this.customFeign = customFeign;
    }

    /**
     * POST请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    @Override
    public String postRequest(String url, Map<String, Object> header, String content) {
        try {
            return customFeign.postRequest(new URI(url), header, content);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            log.error("远程调用异常:{}", e.getMessage());
            throw new RuntimeException("postExecute exception:" + e.getMessage());
        }
    }


    /**
     * GET请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    @Override
    public String getRequest(String url, Map<String, Object> header, String content) {
        try {
            return customFeign.getRequest(new URI(url), header, content);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            log.error("远程调用异常:{}", e.getMessage());
            throw new RuntimeException("postExecute exception:" + e.getMessage());
        }
    }
}

FeignConfiguration 配置类

import feign.Feign;
import feign.Retryer;
import feign.Target;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.slf4j.Slf4jLogger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * feign配置
 *
 * @author Heng.Wei
 * @date 2022/4/19 13:52
 **/
@Slf4j
@Configuration
@Import(FeignClientsConfiguration.class)
public class FeignConfiguration {

    /**
     * 未做请求加解密的 正常的 feign 通信
     *
     * @return com.mea.pay.api.infrastructure.CustomFeign
     * @author Heng.Wei
     * @date 2022/4/20 11:46
     **/
    @Bean("customFeign")
    public CustomFeign custFeign(Decoder decoder, Encoder encoder) {
        return Feign.builder().encoder(encoder).decoder(decoder)
                .retryer(Retryer.NEVER_RETRY)
                .target(Target.EmptyTarget.create(CustomFeign.class));
    }

    /**
     * 第三方平台的feign请求
     * 走我方平台统一的 加解密协议
     * <p>
     * 这里暂时让业务服务节点自己实现 IFeignDecoder 和 IFeignEncoder 接口来使用 partnerFeign
     * 因为 partner 这块对应各个厂商的 密钥管理、如何从缓存中获取对应密钥 还没弄,弄完了的话可以再改造成通用的
     *
     * @return com.mea.pay.api.infrastructure.CustomFeign
     * @author Heng.Wei
     * @date 2022/4/19 14:33
     **/
    @Bean("partnerFeign")
    @ConditionalOnBean({IFeignDecoder.class, IFeignEncoder.class})
    public CustomFeign partnerFeign(IFeignDecoder decoder, IFeignEncoder encoder) {
        return Feign.builder().logLevel(Logger.Level.FULL)
        		.encoder(encoder).decoder(decoder)
                .retryer(Retryer.NEVER_RETRY)
                .target(Target.EmptyTarget.create(CustomFeign.class));
    }

    @Bean
    public feign.Logger logger() {
        return new Slf4jLogger();
    }

}

IFeignClientService 接口定义

package com.mea.pay.api.infrastructure;

import java.util.Map;

/**
 * 动态feignClient - 请求远程API
 * @author Heng.Wei
 * @date 2022/4/19 14:45
 **/
public interface IFeignClientService {

    /**
     * POST请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    String postRequest(String url, Map<String, Object> header, String content);

    /**
     * GET请求远程API
     *
     * @param url 远程目标地址
     * @param header httpheader 请求头参数
     * @param content requestbody 请求体
     * @return java.lang.String requestbody 响应内容
     * @author Heng.Wei
     * @date 2022/4/19 14:45
     **/
    String getRequest(String url, Map<String, Object> header, String content);
}

IFeignDecoder 接口定义

package com.mea.pay.api.infrastructure;

import feign.codec.Decoder;

/**
 * 自定义 feign 解密实现
 * @author Heng.Wei
 * @date 2022/4/20 11:01
 **/
public interface IFeignDecoder extends Decoder {
}

IFeignEncoder 接口定义

package com.mea.pay.api.infrastructure;

import feign.codec.Encoder;

/**
 * 自定义 feign 加密实现
 * @author Heng.Wei
 * @date 2022/4/20 11:01
 **/
public interface IFeignEncoder extends Encoder {
}



5. 使用

如下图所示,只需引入具体的service即可

  • feignClientEncryptedService 走平台统一加解密
  • feignClientService 正常的feign通信

在这里插入图片描述

亲测OK

觉得有帮助的同学请点赞哦( ̄▽ ̄)”



版权声明:本文为qq_33803102原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。