一、注解(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))); } }