当前位置: 首页 > news >正文

使用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>

项目结构目录

在这里插入图片描述

http://www.dtcms.com/a/273182.html

相关文章:

  • 从零开始的语言模型构建 CS336 第一课(一)
  • 【Python练习】036. 编写一个函数,将一个字符串中的所有字符按ASCII值排序
  • 用OpenCV标定相机内参应用示例(C++和Python)
  • Git简单命令
  • 获取印度股票数据API实战指南:NSE与BSE双市场对接
  • 华为OD 周末爬山
  • upload-labs靶场通关详解:第21关 数组绕过
  • 微服务架构下的自动化测试策略调优经验分享
  • 【基于大模型 + FAISS 的本地知识库与智能 PPT 生成系统:从架构到实现】
  • Datawhale AI 夏令营:用户洞察挑战赛 Notebook(2)
  • HVV注意事项(个人总结 非技术)
  • 【HTTP服务端】Cookie?Session?Token?
  • React 自定义Hook——页面或元素滚动到底部监听 Hook
  • Java+Vue开发的资产设备全周期管理系统,移动端+后台管理,涵盖采购至报废全程,实现高效管理、成本可控与资源优化
  • Shell脚本一键部署KubeSphere前置环境
  • 04-ES6
  • 多线程 JAVA
  • Java :Optional容器类
  • python的保险业务管理与数据分析系统
  • AI 智能体:从辅助工具到自主决策者
  • 【YOLO脚本】对模型yaml文件测试
  • ZYNQ MPSOC PL端DDR4读写--仿真(3)
  • JDK的Closure闭包详解
  • 发现和发明浅谈
  • 2025年最新Dubbo-admin 部署
  • HTML初学者第四天
  • Android 应用常见安全问题
  • JavaScript基础(三)
  • 一文讲清楚React Hooks
  • 解决问题的“测地线”:关于第一性原理与其他系统思考框架