记录定时任务中调用feign接口认证不通过的一次bug

一、问题描述

场景是当时处理一个每天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());
        }
    }

}

三、参考资料

https://blog.csdn.net/LOVE320722/article/details/121407610

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨~旋律

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值