一、问题描述
场景是当时处理一个每天0点定时检查xx有效期有没有小于一个月(同样通过相应feign接口去调用查询有效期),如果小于等于一个月,则调用相应的feign接口去重新申请。
@Override
public void execute(ShardingContext shardingContext) {
try {
logger.info("-------------------------开始查询xxx有效期-------------------------");
Date endTime=XXClient.getEndTime();
Date now =new Date();
long day1 = (endTime.getTime() - now.getTime()) / 24 / 60 / 60 / 1000;
if(day1<=30) {
//调用申请xx的方法
XXClient.applyXX();
logger.info("申请成功");
}
}catch (Exception e){
logger.error(e.getMessage());
}
}
然后运行后报空指针:
二、定位问题
然后打日志发现,它只走了第一行的打印日志,第二行调用feign接口就已经报了该异常。
那么问题就很明显定位在feign接口这儿了,后面咨询同事才知道:
定时任务在项目启动的时候就会执行,导致request为空,那么如果不去额外配置网关的话,肯定是会携带这个空的request,那么自然token也就为空,带着空的token去进行权限校验的,那么肯定就报出空指针。
别人的贴子debug也可看到:
其实同上面其他人帖子一样,我们公司对feign的配置也是如此,配置中加了对feign的拦截器其实简单来说分两步:
①从request中获取要调用的其他服务中token的值
②将这个token的值赋给该服务所用的认证请求头
那么问题就出现在获取request,原因和上面的图是一模一样的。
那么我们公司也有对应的解决办法,既然这个拦截器行不通,我自己写一个新的拦截器,不用request的token,我自己做一个自己的自签自验,所以我们公司先自己做了一个拦截器,往这个拦截器增加两个请求头,分别放一个uuid以及通过私钥加密后的uuid值,然后在gateway自制了一个权限验证的过滤器keyPair,在这个keyPair对之前两个请求头进行校验完成自签自验,然后只需在gateway中配置需要跳过权限验证的路径即可,指定过滤器为刚才提到的keyPair.
该代码只是一个解决方案,是一个伪代码,已对代码做相关脱敏
@Configuration
public class FeignInterceptor implements RequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(FeignInterceptor.class);
private static StringBuilder privateKey;
@PostConstruct
//从私钥文件获取私钥
private void getPrivateKey() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(
Objects.requireNonNull(Thread.currentThread().getContextClassLoader().
getResourceAsStream("private.pem")))))) {
privateKey = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("PRIVATE KEY")) {
continue;
}
privateKey.append(line);
}
} catch (IOException e) {
logger.warn("Init privateKey failed,{}", e.getMessage());
}
}
@Override
public void apply(RequestTemplate template) {
try {
String HEADER1 = "HEADER1";
String HEADER2 = "HEADER2";
String uuid = UUID.randomUUID().toString();
//给HEADER1赋值UUID的值
template.header(HEADER1, uuid);
template.header("Content-Type: application/x-www-form-urlencoded");
//给HEADER2赋值通过私钥加密的uuid值
template.header(HEADER2, getPrivateKey(uuid));
} catch (Exception e) {
logger.warn("Init Signature failed,{}", e.getMessage());
}
}
// 私钥加密
public static String encryptByPrivateKey(String content) throws Exception {
// 获取私钥
byte[] keyBytes = base64Decoder.decodeBuffer(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] cipherText = cipher.doFinal(content.getBytes());
String cipherStr = base64Encoder.encode(cipherText);
return cipherStr;
}
可以看到这个拦截器就绕过了request,直接加请求头,这样就巧妙的绕过了空指针的问题
然后我们就可以通过gateway的配置对feign需要调用的相关路径设置一个过滤器,然后在gateway写一个xxxKeyPairFactory,在该工厂类对上面加入的两个请求头进行相关校验,完成自签自验,这里代码不做相关展示,主要是分享这个解决思路。
主要的问题就是因为在定时任务调用feign的时候,feign配置中使用了request进而触发空指针,那么对于空指针我们可以绕过它解决,类似上面的解决方案,也可以通过下面这种直接对着空指针来,request为空,我给你mock一个request出来类似的解决方案,其实都可以解决,看大家如何选择了。
在这里分享一个别人解决的方法:相当于是直接对于attribute为空的情形做了一个补偿机制
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes == null) {
**RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));//为空进行新建**
}
HttpServletRequest request = attributes.getRequest();
// 对消息头进行配置
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
/**
* 判断有没有token,如果是定时任务进来,是没有token的,此时用默认token以调用到对应的Feign服务
*/
boolean flag = true;
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
if(name.equals("token")){
flag = false;
}
template.header(name, values);
}
if(flag){
template.header("token","!@#$%^&*()Cid6032001_Feign");
}
}
// 对请求体进行配置
Enumeration<String> bodyNames = request.getParameterNames();
StringBuffer body =new StringBuffer();
if (bodyNames != null) {
while (bodyNames.hasMoreElements()) {
String name = bodyNames.nextElement();
String values = request.getParameter(name);
body.append(name).append("=").append(values).append("&");
}
}
if(body.length()!=0) {
body.deleteCharAt(body.length()-1);
template.body(body.toString());
}
}
}