自定义注解+aop(接口防刷、身份认证)

一、注解(Annotation

**注解就是一个标记,一个元数据(描述数据的数据),作用于类、方法、参数、变量、构造器及包声明中的特殊修饰符。**如@Override 标记是一个重写方法,如果父类不存在编译器会报错。如果不用@Override 进行标记,当你不小心拼写错想要重写的方法是,依然能够变成成功。

自定义注解:

@Documented
//定义注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NeedLogin {
}
  • @Documented :表明这个注解是由 javadoc记录,好像没啥用处。
  • @Retention :定义被标记的注解能保留多久。
    • SOURCE:编译器忽略
    • CLASS:默认(没有@Retention 采用的策略),保留在Class文件中,但在运行时并不会被VM保留。
    • RUNTIME:运行时依然在,就可以用作反射啦。(常用)
  • @Target :修饰范围。

二、aop(主Spring学习)

概念

AOP(Aspect Oriented Programming面向切面编程)

把一些公共的逻辑业务抽取出来成一个单独的方法,在需要用到该方法时在合适的位置(执行前、执行时、执行后、返回值后、异常后)切进去。

术语含义
横切关注点从每个方法中抽取出来的同一类非核心业务
切面(Aspect)封装横切关注点信息的类,每个关注点体现为一个通知方法
通知(Advice)切面必须要完成的各个具体工作
目标(Target)被通知的对象
连接点(Joinpoint)横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置
切入点(pointcut)执行或找到连接点的一些方式

比如很多接口需要身份认证:

通知注解

@Before前置通知,在方法执行之前执行
@After后置通知,在方法执行之后执行
@AfterRunning返回通知,在方法返回结果之后执行
@AfterThrowing异常通知,在方法抛出异常之后执行
@Around环绕通知,围绕着方法执行

三、简单的应用

1. 接口防刷

  • 自定义注解@AccessRestriction

    /**
     * @Author: Yolo
     * @Date: 2023/04/26/10:31
     * @Description: 访问限制 - 接口防刷
     */
    @Retention(RetentionPolicy.RUNTIME) 
    @Target({ElementType.METHOD,ElementType.TYPE}) 
    public @interface AccessRestriction {
        /**
         * 秒
         * @return 多少秒内
         */
        long second() default 5L;
    
        /**
         * 最大访问次数
         * @return 最大访问次数
         */
        long maxTime() default 3L;
    
        /**
         * 禁用时长,单位/秒
         * @return 禁用时长
         */
        long forbiddenTime() default 120L;
    }
    
  • 切面

    @Aspect
    @Component
    public class AccessRestrictionAspect {
        private static final Logger log = LoggerFactory.getLogger(RPanLoginAspect.class);
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
        /**
         * 锁住时的key前缀
         */
        public static final String LOCK_PREFIX = "LOCK";
    
        /**
         * 统计次数时的key前缀
         */
        public static final String COUNT_PREFIX = "COUNT";
    
        /**
         * 切点入口
         */
        private final String POINT_CUT = "@annotation(com.tao.annotation.AccessRestriction)";
    
        /**
         * 切点
         */
        @Pointcut(value = POINT_CUT)
        public void passAuth(){
    
        }
    
    //    ProceedingJoinPoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。
    //    环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的
    //    暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关)
        // 前置通知、在切点方法之前执行
    		// @Around("@annotation(com.tao.annotation.AccessRestriction) && execution(* initGbo*(..))")
        @Around("passAuth()")
        public Object doPassAuth(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = signature.getMethod();
            AccessRestriction pd = method.getAnnotation(AccessRestriction.class);
            //时间范围
            long second = pd.second();
            //最大访问数
            long maxTime = pd.maxTime();
            //禁用时长
            long forbiddenTime = pd.forbiddenTime();
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            String queryString = request.getQueryString(); //请求参数 ?后面
            //String getQueryString()
    
            log.info("=========ip是{},======uri是{}==={}",ip,uri,queryString);
            if (isForbindden(second, maxTime, forbiddenTime, ip, uri,queryString)) {
    //            throw new RuntimeException("请勿操作那么快,稍等一下!");
                return R.fail(ResponseCode.ACCESS_LIMIT.getCode(), ResponseCode.ACCESS_LIMIT.getDesc());
            }
    
            return proceedingJoinPoint.proceed();
        }
    
        /**
         * 判断某用户访问某接口是否已经被禁用/是否需要禁用
         *
         * @param second        多长时间  单位/秒
         * @param maxRequestCount       最大访问次数
         * @param forbiddenTime 禁用时长 单位/秒
         * @param ip            访问者ip地址
         * @param uri           访问的uri
         * @return ture为需要禁用
         */
        private boolean isForbindden(long second, long maxRequestCount, long forbiddenTime, String ip, String uri,String queryString) {
            String lockKey = LOCK_PREFIX + ip + uri+queryString; //如果此ip访问此uri被禁用时的存在Redis中的 key
            Object isLock = redisTemplate.opsForValue().get(lockKey);
            // 判断此ip用户访问此接口是否已经被禁用
            if (Objects.isNull(isLock)) {
                // 还未被禁用
                String countKey = COUNT_PREFIX + ip + uri+queryString;
                Object count = redisTemplate.opsForValue().get(countKey);
                if (Objects.isNull(count)) {
                    // 首次访问
                    log.info("首次访问");
                    redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                } else {
                    // 此用户前一点时间就访问过该接口,且频率没超过设置
                    if ((Integer) count < maxRequestCount) {
                        redisTemplate.opsForValue().increment(countKey);
                    } else {
                        log.info("{}禁用访问{}", ip, uri);
                        // 禁用
                        redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                        // 删除统计--已经禁用了就没必要存在了
                        redisTemplate.delete(countKey);
                        return true;
                    }
                }
            } else {
                return true;
            }
            return false;
        }
    }
    

2. 身份认证

  • 自定义注解@NeedLogin

    /**
     * 接口需要登录url标识注解
     */
    @Documented
    //定义注解的生命周期
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface NeedLogin {
    }
    
  • 切面

  • /**
     * 请求登录验证
     */
    @Aspect
    @Component
    public class RPanLoginAspect {
    
        private static final Logger log = LoggerFactory.getLogger(RPanLoginAspect.class);
    
        /**
         * 登录认证参数名称
         */
        private static final String LOGIN_AUTHENTICATION_PARAM_NAME = "authorization";
    
        /**
         * 请求头token的key
         */
        private final static String TOKEN_KEY = "Authorization";
    
        /**
         * 切点入口
         */
        private final String POINT_CUT = "@annotation(com.tao.annotation.NeedLogin)";
    
        @Autowired
        @Qualifier(value = "cacheManager")
        private CacheManager cacheManager;
    
        /**
         * 切点
         */
        @Pointcut(value = POINT_CUT)
        public void loginAuth() {
        }
    
        @Around("loginAuth()")
        public Object loginAuth(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = servletRequestAttributes.getRequest();
            String uri = request.getRequestURI();
            log.debug("成功拦截到请求,uri为:{}", uri);
            if (!checkAndSaveUserId(request)) {
                log.warn("成功拦截到请求,uri为:{}, 检测到用户未登录,将跳转至登录页面", uri);
                return R.fail(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getDesc());
            }
            log.debug("成功拦截到请求,uri为:{}, 请求通过", uri);
            return proceedingJoinPoint.proceed();
        }
    
        /**
         * 检查并保存登录用户的ID
         * 此处会实现单设备登录功能 所以本套代码未考虑并发
         *
         * @param request
         * @return
         */
        private boolean checkAndSaveUserId(HttpServletRequest request) {
            String token = request.getHeader(TOKEN_KEY);
            if (StringUtils.isBlank(token)) {
                token = request.getParameter(LOGIN_AUTHENTICATION_PARAM_NAME);
            }
            if (StringUtils.isBlank(token)) {
                return false;
            }
            Object userId = JwtUtil.analyzeToken(token, CommonConstant.LOGIN_USER_ID);
            if (Objects.isNull(userId)) {
                return false;
            }
            Object redisValue = cacheManager.get(CommonConstant.USER_LOGIN_PREFIX + userId);
            if (Objects.isNull(redisValue)) {
                return false;
            }
            if (Objects.equals(redisValue, token)) {
                saveUserId(userId);
                return true;
            }
            return false;
        }
    
        /**
         * 保存用户ID到对应线程上
         *
         * @param userId
         */
        private void saveUserId(Object userId) {
            UserIdUtil.set(Long.valueOf(String.valueOf(userId)));
        }
    
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在JeecgBoot中,使用Shiro进行权限控制和路由控制,配置接口需要以下步骤: 1. 在Shiro配置文件中配置过滤器链:Shiro的过滤器链决定了请求的处理流程,需要在Shiro配置文件中配置过滤器链。可以使用Shiro提供的过滤器来实现不同的权限控制。 2. 配置接口权限:在Shiro配置文件中,可以使用Shiro提供的注解来配置接口的访问权限。例如,@RequiresPermissions注解可以限制只有具有指定权限的用户才能访问该接口。 3. 配置路由:JeecgBoot使用Shiro进行路由控制,需要在Shiro配置文件中配置路由信息。可以使用Shiro提供的PathMatchingFilter来实现路由控制。 下面是一个简单的Shiro配置示例,用于控制/user接口的访问权限: ```xml <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/index"/> <property name="unauthorizedUrl" value="/403"/> <property name="filterChainDefinitions"> <value> /login = anon /** = authc </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> </bean> <bean id="userRealm" class="com.jeecg.boot.modules.user.realm.UserRealm"> <property name="credentialsMatcher" ref="hashedCredentialsMatcher"/> </bean> <bean id="hashedCredentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="proxyTargetClass" value="true"/> </bean> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"/> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionDAO" ref="sessionDAO"/> </bean> ``` 在这个示例中,/login接口允许匿名访问,其他接口需要进行身份认证。可以看到,在filterChainDefinitions中使用了/** = authc的配置,表示所有接口都需要进行身份认证。另外,可以在UserRealm中实现自定义的身份验证逻辑和授权逻辑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值