Spring AOP实现鉴权功能Spring AOP(面向切面编程)是Spring框架的一部分,用于在不改变业务逻辑的情
一、AOP概述以及思想
Spring AOP(面向切面编程)是Spring框架的一部分,用于在不改变业务逻辑的情况下,为应用程序添加横切关注点(cross-cutting concerns)。横切关注点是指那些在多个地方都需要实现的功能,例如日志记录、安全控制、事务管理等。
通俗来说:Spring AOP是一种编程方式,用于在不改变业务逻辑的前提下,轻松地为应用程序添加日志记录、事务管理等公共功能。
为什么要用AOP来实现?
- 如果有好多个接口都鉴权,那这几个接口里面都要加上鉴权的逻辑。代码比较冗余,而且如果这个鉴权的逻辑要修改的话,所有用到的地方都要同步修改,非常麻烦,不好维护。
- 使用切面来把这些接口增强以后,我只需要在用到的地方加上一个注解,功能就实现了,修改起来也方便。
一般来说,配合自定义注解来实现;在需要增加公共功能的方法上打上注解即可;
下面,我们来看看自定义注解如何实现
二、自定义注解
需要自定义注解,而这就需要用到元注解;元注解的作用就是用于定义其它的注解
@Retention
@Retention
(opens new window)指明了注解的保留级别。
@Retention
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
RetentionPolicy
是一个枚举类型,它定义了被 @Retention
修饰的注解所支持的保留级别:
RetentionPolicy.SOURCE
- 标记的注解仅在源文件中有效,编译器会忽略。RetentionPolicy.CLASS
- 标记的注解在 class 文件中有效,JVM 会忽略。RetentionPolicy.RUNTIME
- 标记的注解在运行时有效。
@Retention
示例:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
@Documented
@Documented
(opens new window)表示无论何时使用指定的注解,都应使用 Javadoc(默认情况下,注释不包含在 Javadoc 中)。更多内容可以参考:Javadoc tools page (opens new window)。
@Documented
示例:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
@Target
@Target
(opens new window)指定注解可以修饰的元素类型。
@Target
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
ElementType
是一个枚举类型,它定义了被 @Target
修饰的注解可以应用的范围:
ElementType.ANNOTATION_TYPE
- 标记的注解可以应用于注解类型。ElementType.CONSTRUCTOR
- 标记的注解可以应用于构造函数。ElementType.FIELD
- 标记的注解可以应用于字段或属性。ElementType.LOCAL_VARIABLE
- 标记的注解可以应用于局部变量。ElementType.METHOD
- 标记的注解可以应用于方法。ElementType.PACKAGE
- 标记的注解可以应用于包声明。ElementType.PARAMETER
- 标记的注解可以应用于方法的参数。ElementType.TYPE
- 标记的注解可以应用于类的任何元素。
@Target
示例:
@Target(ElementType.TYPE)
public @interface Table {
/**
* 数据表名称注解,默认值为类名称
* @return
*/
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {}
@Inherited
@Inherited
(opens new window)表示注解类型可以被继承(默认情况下不是这样) 。
表示自动继承注解类型。 如果注解类型声明中存在 @Inherited
元注解,则注解所修饰类的所有子类都将会继承此注解。
🔔 注意:
@Inherited
注解类型是被标注过的类的子类所继承。类并不从它所实现的接口继承注解,方法并不从它所覆写的方法继承注解。此外,当
@Inherited
类型标注的注解的@Retention
是RetentionPolicy.RUNTIME
,则反射 API 增强了这种继承性。如果我们使用java.lang.reflect
去查询一个@Inherited
类型的注解时,反射代码检查将展开工作:检查类和其父类,直到发现指定的注解类型被发现,或者到达类继承结构的顶层。
@Inherited
public @interface Greeting {
public enum FontColor{ BULE,RED,GREEN};
String name();
FontColor fontColor() default FontColor.GREEN;
}
@Repeatable
@Repeatable
(opens new window)表示注解可以重复使用。
以 Spring @Scheduled
为例:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Schedules {
Scheduled[] value();
}
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
// ...
}
应用示例:
public class TaskRunner {
@Scheduled("0 0/15 * * * ?")
@Scheduled("0 0 12 * ?")
public void task1() {}
}
自定义注解格式
注解的语法格式如下:
public @interface 注解名 {定义体}
三、实践(使用AOP进行鉴权)
1. 先定义自定义注解
/**
* 权限校验
*
* @author sgx
*/
@Target(ElementType.METHOD) //应用范围方法体上
@Retention(RetentionPolicy.RUNTIME) //标记的注解在运行时有效。
public @interface AuthCheck {
/**
* 必须有某个角色
*
* @return
*/
String mustRole() default "";
}
2. 定义authCheck方法内部处理逻辑
@Around
spring容器中共有五种通知类型:
- 前置通知: @Before 在方法执行前执行
- 后置通知 :@After 在方法执行后执行
- 返回通知 :@AfterReturning在方法执行前执行,无论是否出现异常
- 异常通知 :@AfterThrowing在方法执行前执行,出现异常则不执行
- 环绕通知 :@Around可以单独完成以上四个通知
"@annotation(authCheck)")
"@annotation(authCheck)":它会匹配所有被 @AuthCheck
注解标记的方法。也就是说,只有被这个注解标记的方法会被 @Around
通知所拦截。
/**
* 权限校验 AOP
*
* @author sgx
*/
@Aspect
@Component
public class AuthInterceptor {
@Resource
private UserService userService;
/**
* 执行拦截
*
* @param joinPoint
* @param authCheck
* @return
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User loginUser = userService.getLoginUser(request);
System.out.println(loginUser.getUserName());
// 必须有该权限才通过
if (StringUtils.isNotBlank(mustRole)) {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustUserRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
String userRole = loginUser.getUserRole();
// 必须有管理员权限
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
if (!mustRole.equals(userRole)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
}
3. 使用
/**
* 根据 id 获取用户(仅管理员)
*
* @param id
* @param request
* @return
*/
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getById(id);
ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);
return ResultUtils.success(user);
}
转载自:https://juejin.cn/post/7418085899714412559