鉴权认证+aop+注解+过滤feign请求的实例

 更新时间:2022年03月15日 09:07:02   作者:cx372877498  
这篇文章主要介绍了鉴权认证+aop+注解+过滤feign请求的实例讲解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

注解类

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    String code() default "";
}

切面

@Aspect
@Component
public class AuthAspect { 
    public static final String FEIGN_FLAG = "YES";
    public static final String URL = "http://service/xxxx";
 
    @Autowired
    private RestTemplate restTemplate;
 
    @Pointcut("@annotation(com.jvv.csr.service.base.annotation.Auth)")
    public void auAspect(){}
 
    @Before(value = "auAspect() && @annotation(param)")
    public void doBefore(Auth param){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String code = request.getHeader("feign");
        if(FEIGN_FLAG.equals(code)){
            return;
        }
        Long networkId = null;
        String token = null;
        Long scope = null;
        try {
            networkId = Long.valueOf(request.getHeader("networkId"));
            token = request.getHeader("authToken");
            scope = Long.valueOf(request.getHeader("scope"));
        } catch (NumberFormatException e) {
            throw new RuntimeException("认证信息失败,head头信息传入错误:"+ e.getMessage());
        }
        HashMap object = null;
        try {
            MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
            paramMap.add("networkId",networkId);
            paramMap.add("scope",scope);
            paramMap.add("token",token);
            paramMap.add("ecode",param.code());
            object = restTemplate.postForObject(URL,paramMap,HashMap.class);
        } catch (Exception e) {
            throw new RuntimeException("调用3A认证接口异常:"+ e.getMessage());
        }
        if (0 != (Integer) object.get("code")) {
            throw new RuntimeException("调用3A认证接口失败:"+ object.get("msg"));
        }
    }
}

内部feign调用不用认证

@Configuration
public class FeignRequestInterceptorConfig implements RequestInterceptor { 
     @Bean
     @LoadBalanced
     RestTemplate restTemplate(){
         return new RestTemplate();
     }
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("feign","YES");
    }
}

需要认证的接口

    @Auth(code = "co-005-1-1")
    @RequestMapping(value ="" ,method = RequestMethod.POST)
    public ResultVO add(@RequestBody  GoodsAllInfoInsertParam insertParam){
 
        ResultVO resultVO = new ResultVO(CodeEnum.SUCCESS,goodsService.addInfo(insertParam));
        return resultVO;
    }

feign aop切不到的诡异案例

我曾遇到过这么一个案例

使用 Spring Cloud 做微服务调用,为方便统一处理 Feign,想到了用 AOP 实现,即使用 within 指示器匹配 feign.Client 接口的实现进行 AOP 切入。代码如下,通过 @Before 注解在执行方法前打印日志,并在代码中定义了一个标记了@FeignClient 注解的 Client 类,让其成为一个 Feign 接口:

package org.geekbang.time.commonmistakes.springpart2.aopfeign.feign; 
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
 
@FeignClient(name = "client")
public interface Client {
    @GetMapping("/feignaop/server")
    String api();
}
package org.geekbang.time.commonmistakes.springpart2.aopfeign; 
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@EnableFeignClients(basePackages = "org.geekbang.time.commonmistakes.springpart2.aopfeign.feign")
public class Config {
}
package org.geekbang.time.commonmistakes.springpart2.aopfeign; 
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
@Aspect
@Slf4j
@Component
public class WrongAspect {
    @Before("within(feign.Client+)")
    public void before(JoinPoint pjp) {
        log.info("within(feign.Client+) pjp {}, args:{}", pjp, pjp.getArgs());
    }
}

通过 Feign 调用服务后可以看到日志中有输出,的确实现了 feign.Client 的切入,切入的是 execute 方法:

[15:48:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@5c16561a]

一开始这个项目使用的是客户端的负载均衡,也就是让 Ribbon 来做负载均衡,代码没啥问题。后来因为后端服务通过 Nginx 实现服务端负载均衡,所以开发同学把@FeignClient 的配置设置了 URL 属性,直接通过一个固定 URL 调用后端服务:

package org.geekbang.time.commonmistakes.springpart2.aopfeign.feign; 
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
 
@FeignClient(name = "anotherClient", url = "http://localhost:45678")
public interface ClientWithUrl {
    @GetMapping("/feignaop/server")
    String api();
}

但这样配置后,之前的 AOP 切面竟然失效了,也就是 within(feign.Client+) 无法切入ClientWithUrl 的调用了。为了还原这个场景,我写了一段代码,定义两个方法分别通过 Client 和 ClientWithUrl 这两个 Feign 进行接口调用:

package org.geekbang.time.commonmistakes.springpart2.aopfeign; 
import lombok.extern.slf4j.Slf4j;
import org.geekbang.time.commonmistakes.springpart2.aopfeign.feign.Client;
import org.geekbang.time.commonmistakes.springpart2.aopfeign.feign.ClientWithUrl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@Slf4j
@RequestMapping("feignaop")
@RestController
public class FeignAopConntroller {
 
    @Autowired
    private Client client;
 
    @Autowired
    private ClientWithUrl clientWithUrl;
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @GetMapping("client")
    public String client() {
        return client.api();
    }
 
    @GetMapping("clientWithUrl")
    public String clientWithUrl() {
        return clientWithUrl.api();
    }
 
    @GetMapping("server")
    public String server() {
        return "OK";
    }
}

可以看到,调用 Client 后 AOP 有日志输出,调用 ClientWithUrl 后却没有:

[15:50:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@5c16561

这就很费解了。难道为 Feign 指定了 URL,其实现就不是 feign.Clinet 了吗?要明白原因,我们需要分析一下 FeignClient 的创建过程,也就是分析FeignClientFactoryBean 类的 getTarget 方法。源码第 4 行有一个 if 判断,当 URL 没有内容也就是为空或者不配置时调用 loadBalance 方法,在其内部通过 FeignContext 从容器获取 feign.Client 的实例:

<T> T getTarget() {
  FeignContext context = this.applicationContext.getBean(FeignContext.class);
  Feign.Builder builder = feign(context);
  if (!StringUtils.hasText(this.url)) {
  ...
  return (T) loadBalance(builder, context,
  new HardCodedTarget<>(this.type, this.name, this.url));
}.
..
  String url = this.url + cleanPath();
  Client client = getOptional(context, Client.class);
  if (client != null) {
  if (client instanceof LoadBalancerFeignClient) {
 // not load balancing because we have a url,
  // but ribbon is on the classpath, so unwrap
  client = ((LoadBalancerFeignClient) client).getDelegate();
}builder.client(client);
}.
..
}protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
  HardCodedTarget<T> target) {
  Client client = getOptional(context, Client.class);
  if (client != null) {
    builder.client(client);
    Targeter targeter = get(context, Targeter.class);
    return targeter.target(this, builder, context, target);
  }
...
}
protected <T> T getOptional(FeignContext context, Class<T> type) {
   return context.getInstance(this.contextId, type);
}

调试一下可以看到,client 是 LoadBalanceFeignClient,已经是经过代理增强的,明显是一个 Bean:

 所以,没有指定 URL 的 @FeignClient 对应的 LoadBalanceFeignClient,是可以通过feign.Client 切入的。在我们上面贴出来的源码的 16 行可以看到,当 URL 不为空的时候,client 设置为了LoadBalanceFeignClient 的 delegate 属性。

其原因注释中有提到,因为有了 URL 就不需要客户端负载均衡了,但因为 Ribbon 在 classpath 中,所以需要从LoadBalanceFeignClient 提取出真正的 Client。断点调试下可以看到,这时 client 是一个ApacheHttpClient

那么,这个 ApacheHttpClient 是从哪里来的呢?这里,我教你一个小技巧:如果你希望知道一个类是怎样调用栈初始化的,可以在构造方法中设置一个断点进行调试。这样,你就可以在 IDE 的栈窗口看到整个方法调用栈,然后点击每一个栈帧看到整个过程。

用这种方式,我们可以看到,是 HttpClientFeignLoadBalancedConfiguration 类实例化的 ApacheHttpClient:

进一步查看 HttpClientFeignLoadBalancedConfiguration 的源码可以发现,LoadBalancerFeignClient 这个 Bean 在实例化的时候,new 出来一个ApacheHttpClient 作为 delegate 放到了 LoadBalancerFeignClient 中:

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
  SpringClientFactory clientFactory, HttpClient httpClient) {
  ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
  return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory)
} 
 
public LoadBalancerFeignClient(Client delegate,
  CachingSpringLoadBalancerFactory lbClientFactory,
  SpringClientFactory clientFactory) {
  this.delegate = delegate;
  this.lbClientFactory = lbClientFactory;
  this.clientFactory = clientFactory;
}

显然,ApacheHttpClient 是 new 出来的,并不是 Bean,而 LoadBalancerFeignClient是一个 Bean。有了这个信息,我们再来捋一下,为什么 within(feign.Client+) 无法切入设置过 URL 的@FeignClient ClientWithUrl:因此,定义了 URL 的 FeignClient 采用 within(feign.Client+) 无法切入。那,如何解决这个问题呢?有一位同学提出,修改一下切点表达式,通过 @FeignClient 注解来切:

package org.geekbang.time.commonmistakes.springpart2.aopfeign; 
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
@Slf4j
//@Component
public class Wrong2Aspect {
 
    @Before("@within(org.springframework.cloud.openfeign.FeignClient)")
    public void before(JoinPoint pjp) {
        log.info("@within(org.springframework.cloud.openfeign.FeignClient) pjp {}, args:{}", pjp, pjp.getArgs());
    }
}

修改后通过日志看到,AOP 的确切成功了:

[15:53:39.093] [http-nio-45678-exec-3] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspe

但仔细一看就会发现,这次切入的是 ClientWithUrl 接口的 API 方法,并不是client.Feign 接口的 execute 方法,显然不符合预期。

这位同学犯的错误是,没有弄清楚真正希望切的是什么对象。@FeignClient 注解标记在Feign Client 接口上,所以切的是 Feign 定义的接口,也就是每一个实际的 API 接口。而通过 feign.Client 接口切的是客户端实现类,切到的是通用的、执行所有 Feign 调用的execute 方法。那么问题来了,ApacheHttpClient 不是 Bean 无法切入,切 Feign 接口本身又不符合要求。怎么办呢?

经过一番研究发现,ApacheHttpClient 其实有机会独立成为 Bean。查看HttpClientFeignConfiguration 的源码可以发现,当没有 ILoadBalancer 类型的时候,自动装配会把 ApacheHttpClient 设置为 Bean。

这么做的原因很明确,如果我们不希望做客户端负载均衡的话,应该不会引用 Ribbon 组件的依赖,自然没有 LoadBalancerFeignClient,只有 ApacheHttpClient:

@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = tru
protected static class HttpClientFeignConfiguration {
  @Bean
  @ConditionalOnMissingBean(Client.class)
  public Client feignClient(HttpClient httpClient) {
     return new ApacheHttpClient(httpClient);
   }
}

那,把 pom.xml 中的 ribbon 模块注释之后,是不是可以解决问题呢?

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

但,问题并没解决,启动出错误了:

Caused by: java.lang.IllegalArgumentException: Cannot subclass final class feig
at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:657)
at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGe

这里,又涉及了 Spring 实现动态代理的两种方式:Spring Boot 2.x 默认使用 CGLIB 的方式,但通过继承实现代理有个问题是,无法继承final 的类。因为,ApacheHttpClient 类就是定义为了 final

public final class ApacheHttpClient implements Client {

为解决这个问题,我们把配置参数 proxy-target-class 的值修改为 false,以切换到使用JDK 动态代理的方式:

spring.aop.proxy-target-class=false

修改后执行 clientWithUrl 接口可以看到,通过 within(feign.Client+) 方式可以切入feign.Client 子类了。以下日志显示了 @within 和 within 的两次切入:

[16:29:55.303] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspe
[16:29:55.310] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@387550b0]

这下我们就明白了,Spring Cloud 使用了自动装配来根据依赖装配组件,组件是否成为Bean 决定了 AOP 是否可以切入,在尝试通过 AOP 切入 Spring Bean 的时候要注意加上上一讲的两个案例,我就把 IoC 和 AOP 相关的坑点和你说清楚了。除此之外,我们在业务开发时,还有一个绕不开的点是,Spring 程序的配置问题。接下来,我们就看具体吧。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java编写简易贪吃蛇游戏

    java编写简易贪吃蛇游戏

    这篇文章主要为大家详细介绍了java编写简易贪吃蛇游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • JVM如何处理异常深入详解

    JVM如何处理异常深入详解

    异常处理的两大元素:抛出异常、捕获异常,非正常处理的两个方法。下面这篇文章主要给大家介绍了关于JVM如何处理异常的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2019-01-01
  • springboot整合RabbitMQ发送短信的实现

    springboot整合RabbitMQ发送短信的实现

    本文会和SpringBoot做整合,实现RabbitMQ发送短信,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • 老生常谈比较排序之堆排序

    老生常谈比较排序之堆排序

    下面小编就为大家带来一篇老生常谈比较排序之堆排序。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • java使用Hex编码解码实现Aes加密解密功能示例

    java使用Hex编码解码实现Aes加密解密功能示例

    这篇文章主要介绍了java使用Hex编码解码实现Aes加密解密功能,结合完整实例形式分析了Aes加密解密功能的定义与使用方法,需要的朋友可以参考下
    2017-01-01
  • java实现微信企业付款到个人

    java实现微信企业付款到个人

    这篇文章主要为大家详细介绍了java实现微信企业付款到个人功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • Spring实战之使用Resource作为属性操作示例

    Spring实战之使用Resource作为属性操作示例

    这篇文章主要介绍了Spring实战之使用Resource作为属性,结合实例形式分析了spring载人Resource作为属性相关配置与使用技巧,需要的朋友可以参考下
    2020-01-01
  • Java枚举类型enum的详解及使用

    Java枚举类型enum的详解及使用

    这篇文章主要介绍了Java枚举类型enum的详解及使用的相关资料,需要的朋友可以参考下
    2017-05-05
  • 如何在 Spring Boot 中配置和使用 CSRF 保护

    如何在 Spring Boot 中配置和使用 CSRF 保护

    CSRF是一种网络攻击,它利用已认证用户的身份来执行未经用户同意的操作,Spring Boot 提供了内置的 CSRF 保护机制,可以帮助您防止这种类型的攻击,这篇文章主要介绍了Spring Boot 中的 CSRF 保护配置的使用方法,需要的朋友可以参考下
    2023-09-09
  • SpringBoot项目jar发布后如何获取jar包所在目录路径

    SpringBoot项目jar发布后如何获取jar包所在目录路径

    这篇文章主要介绍了SpringBoot项目jar发布后如何获取jar包所在目录路径,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11

最新评论