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