使用SpringAOP自定义权限控制注解的实现示例
作者:青衫客36
本文主要介绍了使用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 { @Override public 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; } @Override public 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 { @Autowired private TokenInterceptor tokenInterceptor; @Override public 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>
项目结构目录
到此这篇关于使用SpringAOP自定义权限控制注解的实现示例的文章就介绍到这了,更多相关SpringAOP自定义权限控制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!