使用SpringAOP自定义权限控制注解
本文通过自定义注解对 Controller 层的方法实现访问控制,其核心原理是基于 Spring AOP 的面向切面编程机制。系统在运行时由 Spring 生成目标类的动态代理对象,在方法执行前织入权限校验逻辑,从而实现对用户访问权限的统一拦截与验证,确保接口调用符合预设的安全规则。
package per.mjn.rbacdemo.common.security.annotation;public enum Logical
{/*** 必须具有所有的元素*/AND,/*** 只需具有其中一个元素*/OR
}
package per.mjn.rbacdemo.common.security.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 登录认证:只有登录之后才能进入该方法*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface RequiresLogin {}
package per.mjn.rbacdemo.common.security.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 权限认证:必须具有指定权限才能进入该方法*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresPermissions {/*** 需要校验的权限码*/String[] value() default {};/*** 验证模式:AND | OR, 默认AND*/Logical logical() default Logical.AND;
}
package per.mjn.rbacdemo.common.security.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 角色认证:必须具有指定角色标识才能进入该方法*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles {/*** 需要校验的角色标识*/String[] value() default {};/*** 验证逻辑:AND | OR,默认AND*/Logical logical() default Logical.AND;
}
定义异常
package per.mjn.rbacdemo.common.exception;public class NotLoginException extends RuntimeException {public NotLoginException(String message) {super(message);}
}
package per.mjn.rbacdemo.common.exception;public class NotPermissionException extends RuntimeException {public NotPermissionException(String message) {super("无权限访问接口:" + message);}
}
package per.mjn.rbacdemo.common.exception;public class NotRoleException extends RuntimeException {public NotRoleException(String message) {super("缺少角色:" + message);}
}
package per.mjn.rbacdemo.common.security.aspect;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import per.mjn.rbacdemo.common.security.annotation.RequiresLogin;
import per.mjn.rbacdemo.common.security.annotation.RequiresPermissions;
import per.mjn.rbacdemo.common.security.annotation.RequiresRoles;
import per.mjn.rbacdemo.common.security.auth.AuthUtil;import java.lang.reflect.Method;/*** 基于 Spring Aop 的注解鉴权*/
@Aspect
@Component
public class PreAuthorizeAspect {/*** 构建*/public PreAuthorizeAspect() {}/*** 定义AOP签名 (切入所有使用鉴权注解的方法)*/public static final String POINTCUT_SIGN = " @annotation(per.mjn.rbacdemo.common.security.annotation.RequiresLogin) || "+ "@annotation(per.mjn.rbacdemo.common.security.annotation.RequiresPermissions) || "+ "@annotation(per.mjn.rbacdemo.common.security.annotation.RequiresRoles)";/*** 声明AOP签名*/@Pointcut(POINTCUT_SIGN)public void pointcut() {}/*** 环绕切入* * @param joinPoint 切面对象* @return 底层方法执行后的返回值* @throws Throwable 底层方法抛出的异常*/@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 注解鉴权MethodSignature signature = (MethodSignature) joinPoint.getSignature();checkMethodAnnotation(signature.getMethod());try{// 执行原有逻辑Object obj = joinPoint.proceed();return obj;}catch (Throwable e){throw e;}}/*** 对一个Method对象进行注解检查*/public void checkMethodAnnotation(Method method) {// 校验 @RequiresLogin 注解RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);if (requiresLogin != null){AuthUtil.checkLogin();}// 校验 @RequiresRoles 注解RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);if (requiresRoles != null){AuthUtil.checkRole(requiresRoles);}// 校验 @RequiresPermissions 注解RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);if (requiresPermissions != null){AuthUtil.checkPermi(requiresPermissions);}}
}
package per.mjn.rbacdemo.common.security.auth;import per.mjn.rbacdemo.common.exception.NotLoginException;
import per.mjn.rbacdemo.common.exception.NotPermissionException;
import per.mjn.rbacdemo.common.exception.NotRoleException;
import per.mjn.rbacdemo.common.security.annotation.Logical;
import per.mjn.rbacdemo.common.security.annotation.RequiresPermissions;
import per.mjn.rbacdemo.common.security.annotation.RequiresRoles;
import per.mjn.rbacdemo.common.security.context.LoginUserHolder;
import per.mjn.rbacdemo.domain.LoginUser;import java.util.List;public class AuthLogic {// 模拟登录用户(实际开发中从缓存或Token中获取)public LoginUser getLoginUser() {LoginUser user = LoginUserHolder.get();if (user == null) {throw new NotLoginException("用户未登录或token无效");}return user;}public LoginUser getLoginUser(String token) {return getLoginUser(); // 简化处理}public void checkLogin() {if (getLoginUser() == null) {throw new NotLoginException("未登录");}}public void checkRole(RequiresRoles requiresRoles) {String[] roles = requiresRoles.value();Logical logical = requiresRoles.logical();List<String> userRoles = getLoginUser().getRoles();if (logical == Logical.AND) {for (String role : roles) {if (!userRoles.contains(role)) {throw new NotRoleException(role);}}} else {for (String role : roles) {if (userRoles.contains(role)) return;}throw new NotRoleException(String.join(",", roles));}}public void checkPermi(RequiresPermissions requiresPermissions) {String[] perms = requiresPermissions.value();Logical logical = requiresPermissions.logical();List<String> userPerms = getLoginUser().getPermissions();if (logical == Logical.AND) {for (String perm : perms) {if (!userPerms.contains(perm)) {throw new NotPermissionException(perm);}}} else {for (String perm : perms) {if (userPerms.contains(perm)) return;}throw new NotPermissionException(String.join(",", perms));}}
}
package per.mjn.rbacdemo.common.security.auth;import per.mjn.rbacdemo.common.security.annotation.*;
import per.mjn.rbacdemo.domain.LoginUser;public class AuthUtil {public static AuthLogic authLogic = new AuthLogic();public static void checkLogin() {authLogic.checkLogin();}public static void checkRole(RequiresRoles requiresRoles) {authLogic.checkRole(requiresRoles);}public static void checkPermi(RequiresPermissions requiresPermissions) {authLogic.checkPermi(requiresPermissions);}public static LoginUser getLoginUser(String token) {return authLogic.getLoginUser(token);}
}
package per.mjn.rbacdemo.common.security.context;import per.mjn.rbacdemo.domain.LoginUser;public class LoginUserHolder {private static final ThreadLocal<LoginUser> userThreadLocal = new ThreadLocal<>();public static void set(LoginUser loginUser) {userThreadLocal.set(loginUser);}public static LoginUser get() {return userThreadLocal.get();}public static void clear() {userThreadLocal.remove();}
}
本文中的程序只是为了快速验证自定义注解能否起到权限控制的作用,并未连接数据库,所以,在请求拦截器中设计请求用户的信息(以代表当前请求的用户身份),同时为了便于测试,后期可直接在该部分修改用户信息。
package per.mjn.rbacdemo.common.security.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import per.mjn.rbacdemo.common.security.context.LoginUserHolder;
import per.mjn.rbacdemo.domain.LoginUser;import java.util.ArrayList;
import java.util.List;@Component
public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("Authorization");// 实际应用中,token 应该解密+验签+查缓存等if (token != null && token.equals("mock-token")) {LoginUser loginUser = new LoginUser();loginUser.setUsername("testUser");List<String> roles = new ArrayList<>();roles.add("admin");List<String> permissions = new ArrayList<>();
// permissions.add("system:user:query");permissions.add("system:user:create");loginUser.setRoles(roles);loginUser.setPermissions(permissions);LoginUserHolder.set(loginUser);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {LoginUserHolder.clear();}
}
package per.mjn.rbacdemo.common.web;import java.util.HashMap;public class AjaxResult extends HashMap<String, Object> {public static AjaxResult success(Object data) {AjaxResult result = new AjaxResult();result.put("code", 200);result.put("msg", "success");result.put("data", data);return result;}public static AjaxResult error(String msg) {AjaxResult result = new AjaxResult();result.put("code", 500);result.put("msg", msg);return result;}
}
package per.mjn.rbacdemo.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import per.mjn.rbacdemo.common.security.interceptor.TokenInterceptor;@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate TokenInterceptor tokenInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");}
}
package per.mjn.rbacdemo.controller;import org.springframework.web.bind.annotation.*;
import per.mjn.rbacdemo.common.security.annotation.*;
import per.mjn.rbacdemo.common.security.annotation.Logical;
import per.mjn.rbacdemo.common.web.AjaxResult;
import per.mjn.rbacdemo.domain.LoginUser;import java.util.List;@RestController
@RequestMapping("/user")
public class UserController {@RequiresLogin@GetMapping("/profile")public AjaxResult getProfile() {return AjaxResult.success("用户信息");}@RequiresPermissions("system:user:query")@GetMapping("/{userId}")public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) {return AjaxResult.success("查询用户: " + (userId != null ? userId : "全部"));}@RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR)@PostMapping("/create")public AjaxResult createUser() {LoginUser user = new LoginUser();user.setUsername("admin");user.setRoles(List.of("admin", "user"));user.setPermissions(List.of("system:user:query", "system:user:create"));return AjaxResult.success("创建成功");}
}
package per.mjn.rbacdemo.domain;import java.util.List;
import java.util.Set;
import lombok.Data;@Data
public class LoginUser {private String username;private List<String> roles;private List<String> permissions;
}
package per.mjn.rbacdemo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class RbacDemoApplication {public static void main(String[] args) {SpringApplication.run(RbacDemoApplication.class, args);}}
测试自定义权限控制注解
(1)访问时未携带Token
(2)访问时携带Token,但没有该接口的访问权限
(3)访问时携带Token,且拥有该接口的访问权限
此时,需要将TokenInterceptor类中preHandle()方法中的注释去掉
permissions.add("system:user:query");
附:pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.0</version><relativePath/></parent><modelVersion>4.0.0</modelVersion><groupId>per.mjn</groupId><artifactId>RBACDemo</artifactId><version>0.0.1-SNAPSHOT</version><name>RBACDemo</name><description>RBACDemo</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><scope>test</scope></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency><!-- redis依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- lombok 依赖 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- MyBatis 依赖 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><!-- MySQL 依赖 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!-- Java Servlet --><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version></dependency><!-- Jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
项目结构目录