鉴权认证+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 程序的配置问题。接下来,我们就看具体吧。

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

相关文章

  • JPA中@ElementCollection使用示例详解

    JPA中@ElementCollection使用示例详解

    在JPA中,@ElementCollection注解主要用于映射集合属性,例如List、Set或数组等集合属性,以及Map结构的集合属性,每个属性值都有对应的key映射,这篇文章主要介绍了JPA中@ElementCollection使用,需要的朋友可以参考下
    2023-11-11
  • 在SpringBoot中使用lombok的注意事项

    在SpringBoot中使用lombok的注意事项

    这篇文章主要介绍了在SpringBoot中使用lombok的注意事项,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • mybatis-plus查询源码详解

    mybatis-plus查询源码详解

    这篇文章主要介绍了mybatis-plus查询源码解读,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • Java中的反射机制详解

    Java中的反射机制详解

    这篇文章主要介绍了Java中的反射机制详解的相关资料,需要的朋友可以参考下
    2017-06-06
  • 一篇文章轻松了解SpringBoot配置高级

    一篇文章轻松了解SpringBoot配置高级

    大家都知道SpringBoot拥有良好的基因,还能简化编码、配置、部署、监控,也是现在面试必问的一个点,下面这篇文章主要给大家介绍了如何通过一篇文章轻松了解SpringBoot配置高级的相关资料,需要的朋友可以参考下
    2022-11-11
  • java 服务器接口快速开发之servlet详细教程

    java 服务器接口快速开发之servlet详细教程

    Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容
    2021-06-06
  • Maven仓库无用文件和文件夹清理的方法实现

    Maven仓库无用文件和文件夹清理的方法实现

    这篇文章主要介绍了Maven仓库无用文件和文件夹清理的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • JAVA实现打印ascii码表代码

    JAVA实现打印ascii码表代码

    这篇文章主要介绍了JAVA实现打印ascii码表代码,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • java生成随机数的常用方法分析

    java生成随机数的常用方法分析

    这篇文章主要介绍了java生成随机数的常用方法,结合实例形式分析了java生成随机数常用的方法功能与相关使用技巧,需要的朋友可以参考下
    2017-10-10
  • SpringBoot使用注解集成Redis缓存的示例代码

    SpringBoot使用注解集成Redis缓存的示例代码

    这篇文章主要介绍了在 Spring Boot 中使用注解集成 Redis 缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(TestService)以及测试方法(如在 Controller 中的 redisTest 和 redisCache 方法),还对一些关键代码和配置进行了详细说明
    2025-01-01

最新评论