• 【Spring Boot】# 使用AOP实现接口鉴权访问、白名单限制、记录接口访问日志、限制接口请求次数


    1. AOP相关知识

    1.1 基础知识

    AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的技术。

    利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提供程序的可重用性,同时提高了开发的效率

    通知(Advice):通知描述了切面要完成的工作以及何时执行

    • 前置通知(Before):在目标方法执行前,调用通知功能
    • 后置通知(After):在目标方法执行后,调用通知功能,不关心方法的返回结果
    • 返回通知(AfterReturning):在目标方法成功执行后,准备返回结果时,调用通知功能
    • 异常通知(AfterThrowing):在目标方法抛出异常后,调用通知功能
    • 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为

    连接点(JoinPoint):通知功能被应用的时机。可以拿到目标方法的参数、返回值、签名

    • ProceedingJoinPoint:用于环绕通知,是JoinPoint的子接口,在JoinPoint的基础上,增加了两个方法(最多使用)

      Object proceed() throws Throwable   // 执行目标方法 
      Object proceed(Object[] var1) throws Throwable   // 传入的新的参数去执行目标方法 
      
      • 1
      • 2

    切点(Pointcut):切点定义了通知功能被应用的范围。某个方法,还是某个类,还是所有的类…

    切面(Aspect):就是通知和切点的结合,定义了何时、何地应用通知功能

    引入(Introduction):在无需修改现有类的情况下,向现有的类添加新方法或属性。

    织入(Weaving):把切面应用到目标对象并创建新的代理对象的过程

    1.2 Spring中创建切面需要用到的注解

    • @Aspect:用于定义切面(用于类上)
    • @Before:创建前置通知,通知方法会在目标方法之前执行
    • @After:创建后置通知,通知方法会在目标方法返回或抛出异常后执行
    • @AfterReturning:创建返回通知,通知方法会在目标方法返回后执行
    • @AfterThrowing:创建异常通知,通知方法会在目标方法抛出异常后执行
    • @Around:创建环绕通知,通知方法会将目标方法包围起来,在目标方法之前及之后执行
    • @Pointcut:定义切点表达式

    1.3 切点表达式

    制定了通知被应用的范围,具体的格式为:

    execution(访问修饰符  返回值类型  包名.类名.方法名称(方法参数))
    
    • 1
    • 访问修饰符:可省略
    • 返回值类型、包名、类名、方法名称:都可用 * 代替(表示任意的)
    • 包名与类名之间一个点代表当前包下的类两个点代表当前包及其子包下的类
    • 参数列表可以使用两个点,代表任意个数任意参数列表

    示例:com.lwclick.java.controller包下所有类的public方法

    execution(public com.lwlick.java.controller.*.*(..))
    
    • 1

    2. 实现接口鉴权访问

    定义一个接口控制切面,在环绕通知中,获取用户请求头中携带的信息进行鉴别,判断是否允许执行目标方法(其余的功能,都可与该功能合并实现)

    @Aspect
    @Component
    @Order(1)
    public class LoginAccessAspect {
        private static final Logger LOG = LoggerFactory.getLogger(LoginAccessAspect.class);
    
        /**
         * 所有controller下的方法
         */
        @Pointcut("execution(public * com.lwclick.*controller.*(..))")
        public void pointCut() {
        }
    
        /**
         * 登录方法
         */
        @Pointcut("execution(public String com.lwclick.SysController.login(..))")
        public void login() {
        }
    
        /**
         * 拦截除【登录方法】之外的,所有controller下的方法
         *
         * @return
         */
        @Around("pointCut() && !login()")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            // 此处无需判断 requestAttributes 是否为空
            HttpServletRequest request = requestAttributes.getRequest();
    
            // 获取请求头中的 Authorization 信息
            String authorization = request.getHeader("Authorization");
            // TODO: 根据 authorization 信息进行判断
            if ("通过".equals(authorization)) {
                // 通过了判断,可以认为是携带着登录信息进行请求的,则【允许目标方法继续执行】!!!!!!
                return joinPoint.proceed();
            }
    
            // 返回错误信息,此处通过反射使用了 R 或 AjaxResult 的 error方法(若依框架中的R类及AjaxResult类)
            LOG.info("存在未登录系统的请求,IP为:{}", getIpAddress(request));
            LOG.info("存在未登录系统的请求,具体参数为:{}", getParameter(method, joinPoint.getArgs()));
            Class<?> returnType = method.getReturnType();
            Method error = returnType.getMethod("error", String.class);
            return error.invoke(returnType, "未登录系统,禁止访问!");
        }
    
        /**
         * 获取request中的IP
         *
         * @param request
         * @return
         */
        public static String getIpAddress(HttpServletRequest request) {
            if (request == null) {
                return "unknown";
            }
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Forwarded-For");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Real-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
    
            return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
        }
    
        /**
         * 根据方法和传入的参数获取请求参数
         *
         * @param method 具体的方法
         * @param args   参数列表
         * @return
         */
        private Object getParameter(Method method, Object[] args) {
            List<Object> argList = new ArrayList<>();
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < parameters.length; i++) {
                // 将RequestBody注解修饰的参数作为请求参数
                RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
                if (requestBody != null) {
                    argList.add(args[i]);
                }
                // 将RequestParam注解修饰的参数作为请求参数
                RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
                if (requestParam != null) {
                    Map<String, Object> map = new HashMap<>();
                    String key = parameters[i].getName();
                    if (!StringUtils.isEmpty(requestParam.value())) {
                        key = requestParam.value();
                    }
                    map.put(key, args[i]);
                    argList.add(map);
                }
            }
            if (argList.size() == 0) {
                return null;
            } else if (argList.size() == 1) {
                return argList.get(0);
            } else {
                return argList;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    3. 实现白名单限制

    接口鉴权访问的基础上,当获取到用户的请求IP时,从数据库或缓存中取出白名单组,判断该IP是否在白名单组中即可。

    4. 记录接口访问日志

    接口鉴权访问的基础上,通过request获取请求的信息,进行记录即可

    // 获取请求的URL       http://127.0.0.1:8080/api/getName
    String urlStr = request.getRequestURL().toString();
    // 获取请求的方式      POST
    String methodStr = request.getMethod();
    // 获取请求的URI       /api/getName
    String uri = request.getRequestURI();
    // 获取请求的参数  
    getParameter(method, joinPoint.getArgs());      // getParameter,上面【接口鉴权访问】的代码中有
    // 获取方法执行后的结果
    Object result = joinPoint.proceed();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5. 限制接口请求次数

    实现思路:

    • 定义注解,配置频率,将注解放在需要限制调用频率的接口上
    • 定义切面拦截注解,如果拦截到了自定义的注解,则通过redis进行记录,redis的key为 “ip_接口名称”,value为调用次数,拦截到一次值便 +1
    • 如果该次拦截到的请求,通过key获取到的值已经超过了注解中配置的值,则不放行
    • 否则,放行,redis中的值 +1

    5.1 自定义注解

    注意:count 为访问次数,time 为指定时间,结合起来就是在某段时间内,如果请求达到一定次数,则不予响应

    @Retention(RetentionPolicy.RUNTIME) // 运行时
    @Target({ElementType.TYPE, ElementType.METHOD}) // 可以被用在类及方法上
    @Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级
    public @interface LimitRequest {
        /**
         * 允许的请求次数,默认 MAX_VALUE
         *
         * @return
         */
        int count() default Integer.MAX_VALUE;
    
        /**
         * 时间段,单位为毫秒数(和redis统一),默认 1分钟
         *
         * @return
         */
        int time() default 60 * 1000;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5.2 在类或方法上使用注解

    在需要限制请求次数的类或者方法上,添加 @LimitRequest注解:1分钟内,该接口仅允许被调用 2 次

    @LimitRequest(count = 2)
    @GetMapping("/getInfo")
    public Map<String, Object> getInfo(@RequestParam String deptCode) {
        // TODO
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.3 配置切面

    当用户进行请求时,切面拦截到请求开始判断,通过设置 redis的数据,key 为 ip+接口名称,value为已经调用次数,redis数据有效时间为注解定义的时间

    @Aspect
    @Component
    @Order(1)
    public class ApiControllerAspect {
        private static final Logger logger = LoggerFactory.getLogger(ApiControllerAspect.class);
    
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
    
        /**
         * 需要拦截的位置(所有controller中返回值为 Map的)
         */
        @Pointcut("execution(public java.util.Map com.lwclick.controller.*Controller.*(..)) ")
        public void webAspect() {
        }
    
        @Around("webAspect()")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
            // 获取当前请求对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            boolean flag = true;
    
            String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
            HttpServletRequest request = attributes.getRequest();
    
            // 获取类注释
            Class<ApiController> aClass = ApiController.class;
            InterfaceReqLimit classAnnotation = aClass.getAnnotation(InterfaceReqLimit.class);
    
            // 获取方法注释
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            InterfaceReqLimit methodAnnotation = method.getAnnotation(InterfaceReqLimit.class);
            String uri = request.getRequestURI();
    
            if (methodAnnotation != null) {
                flag = validRequestCount(ipAddr, uri.replace("/", "_"), methodAnnotation.count(), methodAnnotation.time());
            } else if (classAnnotation != null) {
                flag = validRequestCount(ipAddr, uri.replace("/", "_"), classAnnotation.count(), classAnnotation.time());
            }
    
            logger.info("{}请求{}超过请求次数,请稍后再试", ipAddr, uri);
            if (flag) {
                return joinPoint.proceed();
            }
    
            return new HashMap<String, Object>() {
                {
                    put("msg", "超过请求次数,请稍后再试");
                }
            };
        }
    
        /**
         * 判断某一个ip请求接口的次数是否超过限制
         *
         * @param ipAddr
         * @param uri
         * @param limitCount
         * @param timeOut
         * @return
         */
        private boolean validRequestCount(String ipAddr, String uri, int limitCount, long timeOut) {
            try {
                // /api/queryInfo
                String redisKey = "req_limit_".concat(ipAddr).concat(uri);
                long count = redisTemplate.opsForValue().increment(redisKey, 1);
                if (count == 1) {
                    redisTemplate.expire(redisKey, timeOut, TimeUnit.MILLISECONDS);
                }
                if (count > limitCount) {
                    return false;
                }
            } catch (Exception e) {
                return false;
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
  • 相关阅读:
    JavaWeb-深度解析转发和重定向
    Java Character.isTitleCase()具有什么功能呢?
    渗透攻击漏洞——原型链污染
    使用C语言实现前,中,后序线索化二叉树
    『干货』WebStorm代码模板配置大全
    k8s 读书笔记 - kubernetes 基本概念和术语(下)
    使用Detectron2目标检测&特征提取
    JUC学习笔记——共享模型之内存
    vue3父子组件传值,子组件暴漏方法
    1407. 排名靠前的旅行者
  • 原文地址:https://blog.csdn.net/qq_38134242/article/details/127942107