@PreAuthorize在private方法、同类调用或非Spring管理对象中失效,需自定义@RequirePermission注解配合反射校验补足;注解须@Retention(RUNTIME)、@Target(METHOD),value用字符串便于动态权限,校验时需处理代理类、方法可见性及参数类型匹配,并通过PermissionService统一鉴权。

为什么直接用 @PreAuthorize 不行,才要自己写注解反射校验
Spring Security 的 @PreAuthorize 确实能做权限控制,但它依赖 SpEL 表达式和完整的 Spring AOP 代理链,一旦方法是 private、被同类内调用、或运行在非 Spring 管理的实例(比如手动 new 出来的对象),它就完全失效。你自己写的注解+反射校验,核心价值不是“替代 Spring”,而是补上这些失控场景——比如工具类静态方法、测试 mock 实例、命令行入口、或者微服务中跨模块直调的 POJO 方法。
@RequirePermission 注解定义和保留策略怎么选
必须声明为 @Retention(RetentionPolicy.RUNTIME),否则反射时拿不到;@Target({ElementType.METHOD}) 就够了,类级权限通常走拦截器更合适;别加 @Documented 或 @Inherited——前者无实际用途,后者对方法注解无效,反而误导人。
示例定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequirePermission {
String value(); // 如 "user:delete"
boolean throwException() default true;
}
-
value()用字符串而非枚举:方便对接动态权限系统(如 DB 查权限码) -
throwException控制失败时是抛异常还是静默返回 null/empty——有些工具方法允许“没权限就跳过”,不必强中断
反射获取注解并校验权限的典型陷阱
最容易错的是没处理代理对象。用 method.getDeclaringClass() 拿到的是原始类,但实际执行时可能是 CGLIB 生成的 UserService$$EnhancerBySpringCGLIB$$xxx。这时直接 clazz.getDeclaredMethod(...) 会报 NoSuchMethodException。
立即学习“Java免费学习笔记(深入)”;
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
正确做法是用 Method method = target.getClass().getMethod(methodName, paramTypes),再从该 method 上 getAnnotation——前提是确保 target 是 Spring 代理后的对象,且方法是 public。
- private 方法无法通过反射
getMethod获取,必须用getDeclaredMethod+setAccessible(true),但生产环境常被 SecurityManager 禁用 - 参数类型必须严格匹配,
Integer.class和int.class被视为不同方法,建议统一用包装类定义注解位置 - 校验逻辑里别硬编码角色名,应调用
PermissionService.hasPermission(subject, permissionCode),把判断权交给统一服务
在非 Spring 环境下手动触发校验的最小可行代码
比如单元测试中 new 了一个 service 实例,想验证加了注解的方法是否真被保护:
public static void checkPermission(Object target, String methodName, Object... args) throws Exception {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
Method method = target.getClass().getMethod(methodName, argTypes);
RequirePermission ann = method.getAnnotation(RequirePermission.class);
if (ann != null && !PermissionChecker.hasPermission(ann.value())) {
if (ann.throwException()) {
throw new AccessDeniedException("Missing permission: " + ann.value());
} else {
return;
}
}
method.invoke(target, args);
}
注意这里没做 try-catch 吞异常——权限校验失败本就应该暴露,而不是静默吞掉再让后续逻辑 NPE。
真正难的不是写这几行反射,而是把 PermissionChecker 的 subject(当前用户身份)可靠地透传进来。线程变量(ThreadLocal)最常用,但要注意在异步(CompletableFuture、线程池)场景下丢失上下文——这点比注解本身还容易被忽略。

















