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

目录
  • 注解类
  • 切面
  • 内部feign调用不用认证
  • 需要认证的接口
  • feignaop切不到的诡异案例
    • 我曾遇到过这么一个案例

注解类

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

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

(0)

相关推荐

  • SpringBoot使用Filter实现签名认证鉴权的示例代码

    情景说明 鉴权,有很多方案,如:SpringSecurity.Shiro.拦截器.过滤器等等.如果只是对一些URL进行认证鉴权的话,我们完 全没必要引入SpringSecurity或Shiro等框架,使用拦截器或过滤器就足以实现需求.         本文介绍如何使用过滤器Filter实现URL签名认证鉴权. 本人测试软硬件环境:Windows10.Eclipse.SpringBoot.JDK1.8 准备工作 第一步:在pom.xml中引入相关依赖 <dependencies> <dep

  • Spring Boot 访问安全之认证和鉴权详解

    目录 拦截器 认证 鉴权 在web应用中有大量场景需要对用户进行安全校,一般人的做法就是硬编码的方式直接埋到到业务代码中,但可曾想过这样做法会导致代码不够简洁(大量重复代码).有个性化时难维护(每个业务逻辑访问控制策略都不相同甚至差异很大).容易发生安全泄露(有些业务可能不需要当前登录信息,但被访问的数据可能是敏感数据由于遗忘而没有受到保护). 为了更安全.更方便的进行访问安全控制,我们可以想到的就是使用springmvc的拦截器(HandlerInterceptor),但其实更推荐使用更为成熟

  • 详解用JWT对SpringCloud进行认证和鉴权

    JWT(JSON WEB TOKEN)是基于RFC 7519标准定义的一种可以安全传输的小巧和自包含的JSON对象.由于数据是使用数字签名的,所以是可信任的和安全的.JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名. JWT通常由头部(Header),负载(Payload),签名(Signature)三个部分组成,中间以.号分隔,其格式为Header.Payload.Signature Header:声明令牌的类型和使用的算法 alg:签名的算法 typ:to

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

    目录 注解类 切面 内部feign调用不用认证 需要认证的接口 feignaop切不到的诡异案例 我曾遇到过这么一个案例 注解类 @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Auth {     String code() default ""; } 切面 @Aspect @Component public class AuthAspect

  • Spring boot通过AOP防止API重复请求代码实例

    这篇文章主要介绍了Spring boot通过AOP防止API重复请求代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 实现思路 基于Spring Boot 2.x 自定义注解,用来标记是哪些API是需要监控是否重复请求 通过Spring AOP来切入到Controller层,进行监控 检验重复请求的Key:Token + ServletPath + SHA1RequestParas Token:用户登录时,生成的Token Servlet

  • 浅析k8s中各组件和kube apiserver通信时的认证和鉴权问题

    目录 背景 kubectl的身份和权限 kubectl用的是什么身份? 能操作哪些资源呢? kube-scheduler的身份和权限 kube-scheduler用的是什么身份? kubelet的身份和权限 kubelet用的是什么身份? kubelet能操作哪些资源? 验证kubelet的权限 calico calico用的是什么身份? pod pod用的是什么身份? 总结 背景 和master节点kube api-server通信的组件有很多,包括: kubelet calico sched

  • springboot做代理分发服务+代理鉴权的实现过程

    还原背景 大家都做过b-s架构的应用,也就是基于浏览器的软件应用.现在呢有个场景就是FE端也就是前端工程是前后端分离的,采用主流的前端框架VUE编写.服务端采用的是springBoot架构. 现在有另外一个服务也需要与前端页面交互,但是由于之前前端与服务端1交互时有鉴权与登录体系逻辑控制以及分布式session存储逻辑都在服务1中,没有把认证流程放到网关.所以新服务与前端交互则不想再重复编写一套鉴权认证逻辑.最终想通过服务1进行一个代理把前端固定的请求转发到新加的服务2上. 怎么实现 思路:客户

  • springCloud gateWay 统一鉴权的实现代码

    目录 一,统一鉴权 1.1鉴权逻辑 1.2代码实现 一,统一鉴权 内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己 编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验. 1.1 鉴权逻辑 开发中的鉴权逻辑: 当客户端第一次请求服务时,服务端对用户进行信息认证(登录) 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证 以后每次请求,客户端都携带认证的token 服务端对token进行解密,判断是否有效

  • SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现

    废话 目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以.SpringBoot的易用性和对其他框架的高度集成,用来快速开发一个小型应用是最佳的选择. 一套前后端分离的后台项目,刚开始就要面对的就是登陆和授权的问题.这里提供一套方案供大家参考. 主要看点: 登陆后获取token,根据token来请求资源 根据用户角色来确定对资源的访问权限 统一异常处理 返回标准的Json格式数据 正文 首先是pom文件: <dependencies

  • 使用Feign设置Token鉴权调用接口

    目录 Feign设置Token鉴权调用接口 声明FeignClient指定url 调用测试 返回对象可以封装demo 先去implementsRequestInterceptor重写apply方法 配置拦截器 Feign调用进行Token鉴权 项目场景 解决办法 具体实现 注意有Bug!!! Feign设置Token鉴权调用接口 声明FeignClient 指定url /**  * CREATE BY songzhongjin ON 2021.05.08 15:58 星期六  * DESC:fe

  • 关于Mongodb 认证鉴权你需要知道的一些事

    前言 本文主要给大家介绍了Mongodb认证鉴权的一些相关内容,通过设置认证鉴权会对大家的mongodb安全进一步的保障,下面话不多说了,来一起看看详细的介绍吧. 一.Mongodb 的权限管理 认识权限管理,说明主要概念及关系 与大多数数据库一样,Mongodb同样提供了一套权限管理机制. 为了体验Mongodb 的权限管理,我们找一台已经安装好的Mongodb,可以参照这里搭建一个单节点的Mongodb. 直接打开mongo shell: ./bin/mongo --port=27017 尝

随机推荐