feign的加解密封装

本文介绍了一种通过覆盖Feign的Encoder和Decoder接口实现请求和响应加解密的方法,用于支付渠道与第三方支付的callback请求。核心代码包括FeignRequestEncoder的请求加密和FeignResponseDecoder的响应解密,采用了RSA和AES加密算法。此外,展示了如何动态使用加密和非加密的FeignClient,并提供了测试示例和关键接口定义。
摘要由CSDN通过智能技术生成

功能描述

通过覆盖 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
觉得有帮助的同学请点赞哦( ̄▽ ̄)"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值