Spring Security 源码解析(八)方法安全授权与自定义扩展:@EnableMethodSecurity、AOP、SpEL 全链路

@EnableMethodSecurity 注解到 @PreAuthorize 的 AOP 拦截,再到 SpEL 表达式解析------方法级授权的底层机制远比表面看到的复杂。本文还将介绍两种生产级自定义授权方案:SpEL 调用自定义 Bean 和自定义 AuthorizationManager。

前言

前一篇我们拆解了 URL 授权的全链路。但 URL 授权只能做到路径级别的粗粒度控制,真正的业务权限往往需要精确到方法级别------"这个用户能不能修改这条记录?"这类问题,URL 授权回答不了。

Spring Security 的方法安全授权就是为此而生。


一、@EnableMethodSecurity:方法安全的启用原理

1.1 启用注解

java 复制代码
@Configuration
@EnableMethodSecurity  // Spring Security 6.x 推荐方式
public class SecurityConfig {
    // ...
}

注意:@EnableGlobalMethodSecurity 在 6.x 中已废弃,由 @EnableMethodSecurity 替代。

1.2 @EnableMethodSecurity 的内部机制

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MethodSecuritySelector.class)  // 唯一的静态导入,其他配置由 Selector 动态决定
public @interface EnableMethodSecurity {
    
    boolean prePostEnabled() default true;   // 启用 @PreAuthorize / @PostAuthorize
    
    boolean securedEnabled() default false;  // 启用 @Secured
    
    boolean jsr250Enabled() default false;   // 启用 @RolesAllowed
    
    boolean proxyTargetClass() default false; // CGLIB vs JDK 动态代理
    
    AdviceMode mode() default AdviceMode.PROXY; // 代理模式:PROXY / ASPECTJ
    
    int offset() default 0;  // 拦截器顺序偏移量(6.3+)
}

1.3 核心组件链路

less 复制代码
@EnableMethodSecurity  @Import(MethodSecuritySelector.class)
       │
       └── MethodSecuritySelector.selectImports()
             │
             ├── AutoProxyRegistrar → 注册 AnnotationAwareAspectJAutoProxyCreator
             ├── MethodSecurityAdvisorRegistrar → 注册切面 Advisor
             │
             ├── [prePostEnabled=true] → PrePostMethodSecurityConfiguration
             │     ├── PreAuthorizeAuthorizationManager
             │     ├── PostAuthorizeAuthorizationManager
             │     ├── PreFilterAuthorizationMethodInterceptor
             │     └── PostFilterAuthorizationMethodInterceptor
             │
             ├── [securedEnabled=true] → SecuredMethodSecurityConfiguration
             ├── [jsr250Enabled=true] → Jsr250MethodSecurityConfiguration
             │
             ├── AuthorizationProxyConfiguration(处理 @AuthorizeReturnObject)
             ├── [条件] AuthorizationProxyDataConfiguration(Spring Data 环境)
             ├── [条件] AuthorizationProxyWebConfiguration(Spring Web 环境)
             └── [条件] MethodObservationConfiguration(Micrometer 环境)

1.4 MethodSecuritySelector 完整源码:动态导入配置类

java 复制代码
final class MethodSecuritySelector implements ImportSelector {
    private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();

    @Override
    public String[] selectImports(AnnotationMetadata importMetadata) {
        if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())
                && !importMetadata.hasMetaAnnotation(EnableMethodSecurity.class.getName())) {
            return new String[0];
        }
        EnableMethodSecurity annotation = importMetadata.getAnnotations()
            .get(EnableMethodSecurity.class).synthesize();
        List<String> imports = new ArrayList<>(
            Arrays.asList(this.autoProxy.selectImports(importMetadata)));

        //  根据注解属性,动态决定导入哪些配置类
        if (annotation.prePostEnabled()) {
            imports.add(PrePostMethodSecurityConfiguration.class.getName());
        }
        if (annotation.securedEnabled()) {
            imports.add(SecuredMethodSecurityConfiguration.class.getName());
        }
        if (annotation.jsr250Enabled()) {
            imports.add(Jsr250MethodSecurityConfiguration.class.getName());
        }
        //  AuthorizationProxyConfiguration:处理 @AuthorizeReturnObject(返回值授权)
        imports.add(AuthorizationProxyConfiguration.class.getName());
        return imports.toArray(new String[0]);
    }

    // 内部类:根据 AdviceMode 导入 AOP 代理注册器
    private static final class AutoProxyRegistrarSelector 
            extends AdviceModeImportSelector<EnableMethodSecurity> {
        private static final String[] IMPORTS = new String[] { 
            AutoProxyRegistrar.class.getName(),           // 注册 AnnotationAwareAspectJAutoProxyCreator
            MethodSecurityAdvisorRegistrar.class.getName() // 注册切面 Advisor
        };
        private static final String[] ASPECTJ_IMPORTS = new String[] { 
            MethodSecurityAspectJAutoProxyRegistrar.class.getName() 
        };
        @Override
        protected String[] selectImports(AdviceMode adviceMode) {
            if (adviceMode == AdviceMode.PROXY) return IMPORTS;
            if (adviceMode == AdviceMode.ASPECTJ) return ASPECTJ_IMPORTS;
            throw new IllegalStateException("AdviceMode not supported");
        }
    }
}

1.5 导入的配置类职责速查

组件 类型 职责
MethodSecuritySelector ImportSelector 根据 @EnableMethodSecurity 属性,动态决定导入哪些配置类
PrePostMethodSecurityConfiguration @Configuration 创建 4 个核心拦截器:preAuthorize / postAuthorize / preFilter / postFilter
AutoProxyRegistrar 注册器 注册 AnnotationAwareAspectJAutoProxyCreator------AOP 代理创建的总指挥
MethodSecurityAdvisorRegistrar 注册器 将方法级安全拦截器包装成 Spring AOP 能识别的 Advisor
AnnotationAwareAspectJAutoProxyCreator BeanPostProcessor 在 Bean 初始化后检查是否需要创建代理,匹配的 Bean 会被包装
AuthorizationProxyConfiguration @Configuration 处理 @AuthorizeReturnObject------返回值对象的细粒度数据访问控制

1.6 AOP 代理创建的完整时序

  1. Spring 容器创建业务 Bean (如 OrderService
  2. AnnotationAwareAspectJAutoProxyCreator (实现了 BeanPostProcessor)在 Bean 初始化后介入
  3. 拿出所有 Advisor,逐一比对 OrderService 的方法
  4. 发现 deleteOrder(Long id) 方法上有 @PreAuthorize 注解 → 匹配成功!
  5. 使用 ProxyFactory 创建代理对象(JDK 动态代理或 CGLIB)
  6. AuthorizationManagerBeforeMethodInterceptor 织入代理对象的拦截器链
  7. Spring 容器中最终持有的 orderService 引用指向这个代理对象

1.7 方法调用时的 AOP 拦截流程

当代理对象上的方法被实际调用时,AOP 的拦截逻辑开始运作:

scss 复制代码
调用 orderService.getOrder(123)
       │
       ▼
AOP 代理(AnnotationAwareAspectJAutoProxyCreator 创建)
       │
       ▼
AuthorizationManagerBeforeMethodInterceptor.invoke()  // 6.x 替代旧版 MethodSecurityInterceptor
       │
       ├── 获取注解 → 匹配 Pointcut(注解匹配或表达式匹配)
       ├── 调用 AuthorizationManager.authorize() 进行授权检查
       │     ├── GRANTED → 继续执行目标方法
       │     └── DENIED → 抛出 AuthorizationDeniedException
       │
       ▼
目标方法执行

二、PrePostMethodSecurityConfiguration:4 个拦截器的诞生地

PrePostMethodSecurityConfiguration 是方法安全的核心配置类,负责创建处理 @PreAuthorize@PostAuthorize@PreFilter@PostFilter 四个注解的 4 个核心拦截器。

核心构造函数

java 复制代码
PrePostMethodSecurityConfiguration(
        ObjectProvider<ObjectPostProcessor<AuthorizationManager<MethodInvocation>>> preAuthorizeProcessor,
        ObjectProvider<ObjectPostProcessor<AuthorizationManager<MethodInvocationResult>>> postAuthorizeProcessor) {
    // ① 设置 ExpressionHandler(表达式解析引擎)
    this.preFilterMethodInterceptor.setExpressionHandler(this.expressionHandler);
    this.preAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler);
    this.postAuthorizeAuthorizationManager.setExpressionHandler(this.expressionHandler);
    this.postFilterMethodInterceptor.setExpressionHandler(this.expressionHandler);

    // ② 构建 preAuthorize 拦截器
    AuthorizationManager<MethodInvocation> preAuthorize = preAuthorizeProcessor
        .getIfUnique(ObjectPostProcessor::identity)
        .postProcess(this.preAuthorizeAuthorizationManager);
    this.preAuthorizeMethodInterceptor = 
        AuthorizationManagerBeforeMethodInterceptor.preAuthorize(preAuthorize);

    // ③ 构建 postAuthorize 拦截器
    AuthorizationManager<MethodInvocationResult> postAuthorize = postAuthorizeProcessor
        .getIfUnique(ObjectPostProcessor::identity)
        .postProcess(this.postAuthorizeAuthorizationManager);
    this.postAuthorizeMethodInterceptor = 
        AuthorizationManagerAfterMethodInterceptor.postAuthorize(postAuthorize);
}

4 个拦截器的注册方式 ------注意它们都是通过 DeferringMethodInterceptor 延迟加载的:

java 复制代码
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
        ObjectProvider<PrePostMethodSecurityConfiguration> config) {
    //  DeferringMethodInterceptor:延迟加载,避免循环依赖
    return new DeferringMethodInterceptor<>(preAuthorizePointcut,
        () -> config.getObject().preAuthorizeMethodInterceptor);
}

为什么用 DeferringMethodInterceptor 因为 PrePostMethodSecurityConfiguration 本身也是 Bean,直接返回拦截器实例容易导致循环依赖。DeferringMethodInterceptor 告诉 AOP 框架"这个切面的切点是 preAuthorizePointcut,具体的拦截逻辑请到时候再找我要"。

2.1 AuthorizationManagerBeforeMethodInterceptor.preAuthorize() 构建拦截器

java 复制代码
public static AuthorizationManagerBeforeMethodInterceptor preAuthorize(
        AuthorizationManager<MethodInvocation> authorizationManager) {
    AuthorizationManagerBeforeMethodInterceptor interceptor = 
        new AuthorizationManagerBeforeMethodInterceptor(
            //  切点:所有带 @PreAuthorize 注解的方法
            AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), 
            authorizationManager);  // PreAuthorizeAuthorizationManager
    interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
    return interceptor;
}

2.2 拦截器的核心拦截逻辑:attemptAuthorization()

AuthorizationManagerBeforeMethodInterceptor 的核心在于 attemptAuthorization() 方法,它负责调用 AuthorizationManager 进行授权决策:

java 复制代码
// invoke → 直接委托给 attemptAuthorization
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    return attemptAuthorization(mi);
}

//  核心授权逻辑
private Object attemptAuthorization(MethodInvocation mi) throws Throwable {
    // ① 调用 AuthorizationManager.authorize() 进行授权决策
    AuthorizationResult result;
    try {
        result = this.authorizationManager.authorize(this::getAuthentication, mi);
    }
    catch (AuthorizationDeniedException denied) {
        return handle(mi, denied);  // 授权管理器直接抛出拒绝异常
    }

    // ② 发布授权事件(可被监控系统采集)
    this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, result);

    // ③ 授权拒绝 → 委托给 MethodAuthorizationDeniedHandler
    if (result != null && !result.isGranted()) {
        return handle(mi, result);
    }

    // ④ 授权通过 → 继续执行目标方法
    return proceed(mi);
}

// 拒绝处理:委托给 MethodAuthorizationDeniedHandler(默认抛 AuthorizationDeniedException)
private Object handle(MethodInvocation mi, AuthorizationResult result) {
    if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
        return handler.handleDeniedInvocation(mi, result);
    }
    return this.defaultHandler.handleDeniedInvocation(mi, result);
}

// 执行目标方法:若方法执行过程中抛出 AuthorizationDeniedException 也会被捕获处理
private Object proceed(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed();
    }
    catch (AuthorizationDeniedException ex) {
        if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
            return handler.handleDeniedInvocation(mi, ex);
        }
        return this.defaultHandler.handleDeniedInvocation(mi, ex);
    }
}

// 从 SecurityContextHolder 获取 Authentication
private Authentication getAuthentication() {
    Authentication authentication = this.securityContextHolderStrategy.get()
        .getContext().getAuthentication();
    if (authentication == null) {
        throw new AuthenticationCredentialsNotFoundException(
            "An Authentication object was not found in the SecurityContext");
    }
    return authentication;
}

三、SpEL 表达式解析全链路:从 hasRole 到权限决策

3.1 PreAuthorizeAuthorizationManager:表达式执行引擎

java 复制代码
public final class PreAuthorizeAuthorizationManager 
        implements AuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {

    private PreAuthorizeExpressionAttributeRegistry registry = 
        new PreAuthorizeExpressionAttributeRegistry();

    @Override
    public AuthorizationDecision check(
            Supplier<Authentication> authentication, MethodInvocation mi) {
        // ① 获取方法上缓存的 SpEL 表达式
        ExpressionAttribute attribute = this.registry.getAttribute(mi);
        if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {  // NULL_ATTRIBUTE 常量
            return null;  // 方法上没有 @PreAuthorize 注解
        }

        // ② 创建 SpEL 评估上下文(包含 authentication + 方法参数 + BeanResolver)
        EvaluationContext ctx = this.registry.getExpressionHandler()
            .createEvaluationContext(authentication, mi);

        // ③  执行 SpEL 表达式,返回 AuthorizationDecision
        return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
    }
}

3.2 AbstractExpressionAttributeRegistry:表达式缓存机制

java 复制代码
abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute> {
    //  ConcurrentHashMap 缓存:方法 → 已解析的表达式
    private final Map<MethodClassKey, T> cachedAttributes = new ConcurrentHashMap<>();
    private MethodSecurityExpressionHandler expressionHandler = 
        new DefaultMethodSecurityExpressionHandler();

    final T getAttribute(MethodInvocation mi) {
        Method method = mi.getMethod();
        Object target = mi.getThis();
        Class<?> targetClass = (target != null) ? target.getClass() : null;
        return getAttribute(method, targetClass);
    }

    final T getAttribute(Method method, Class<?> targetClass) {
        MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
        //  computeIfAbsent:第一次调用时解析并缓存,后续直接从缓存取
        return this.cachedAttributes.computeIfAbsent(cacheKey, 
            (k) -> resolveAttribute(method, targetClass));
    }
}

缓存设计 :同一方法的 SpEL 表达式只会被解析一次,后续调用直接从 ConcurrentHashMap 中获取,避免重复解析的性能开销。

3.3 DefaultMethodSecurityExpressionHandler:构建 SpEL 运行环境

java 复制代码
public class DefaultMethodSecurityExpressionHandler 
        extends AbstractSecurityExpressionHandler<MethodInvocation> {

    @Override
    public EvaluationContext createEvaluationContext(
            Supplier<? extends Authentication> authentication, MethodInvocation mi) {
        // ① 创建根对象(SecurityExpressionRoot),注入 authentication
        MethodSecurityExpressionOperations root = 
            createSecurityExpressionRoot(authentication, mi);
        // ② 创建评估上下文,绑定方法参数为 SpEL 变量(支持 #id, #name 等)
        MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(
            root, mi, getParameterNameDiscoverer());
        // ③ 设置 BeanResolver(支持 @beanName 语法)
        ctx.setBeanResolver(getBeanResolver());
        return ctx;
    }
}

3.4 SecurityExpressionRoot:hasRole 到底调用了什么?

这是理解 SpEL 表达式执行链最关键的一环。 当你写 hasRole('ADMIN') 时,SpEL 引擎最终会调用 SecurityExpressionRoot 上的方法(该方法由 MethodSecurityExpressionRoot 继承):

java 复制代码
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
    private final Supplier<Authentication> authentication;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";  //  默认角色前缀

    // hasRole('ADMIN') → hasAnyRole("ADMIN") → hasAnyAuthorityName("ROLE_", "ADMIN")
    public final boolean hasRole(String role) {
        return hasAnyRole(role);
    }

    public final boolean hasAnyRole(String... roles) {
        return hasAnyAuthorityName(this.defaultRolePrefix, roles);
    }

    public final boolean hasAuthority(String authority) {
        return hasAnyAuthority(authority);
    }

    public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
    }

    //  核心:从 Authentication 中提取权限集合,应用 RoleHierarchy,逐个比对
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();  // ① 懒加载:从 Authentication 获取 authorities
        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role); // ② ROLE_ + ADMIN
            if (roleSet.contains(defaultedRole)) {  // ③ 是否包含 ROLE_ADMIN
                return true;
            }
        }
        return false;
    }

    //  获取权限集合(懒加载 + RoleHierarchy 支持)
    private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            Collection<? extends GrantedAuthority> userAuthorities = getAuthentication().getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return this.roles;
    }
}

关键差异 :与 URL 授权不同,SpEL 中的 hasRole() 不经过 AuthorizationManager 包装,而是直接在表达式中返回 boolean 值。表达式 hasRole('ADMIN') 的返回值直接被 SpEL 引擎解释为授权决策(true = 通过,false = 拒绝)。

3.5 表达式执行全链路总结

less 复制代码
@PreAuthorize("hasRole('ADMIN')")
        │
        ▼
PreAuthorizeAuthorizationManager.check()
        │
        ├── registry.getAttribute(mi) → 从缓存获取 SpEL 表达式
        ├── expressionHandler.createEvaluationContext() → 构建上下文
        │     ├── 创建 MethodSecurityExpressionRoot(继承 SecurityExpressionRoot)
        │     ├── 绑定方法参数为 SpEL 变量(#id)
        │     └── 设置 BeanResolver(支持 @beanName)
        │
        └── ExpressionUtils.evaluate()
              │
              └── SpEL 引擎执行 "hasRole('ADMIN')"
                    │
                    └── SecurityExpressionRoot.hasRole("ADMIN")
                          │
                          └── hasAnyRole("ADMIN")
                                │
                                └── hasAnyAuthorityName("ROLE_", "ADMIN")
                                      │
                                      └── getAuthoritySet() → roleSet.contains("ROLE_ADMIN")
                                            │
                                            ├── true → 返回 true → GRANTED
                                            └── false → 返回 false → DENIED → AuthorizationDeniedException

一句话总结整个链路:"表达式 → SpEL 根对象 → 权限集合比对 → 决策"

3.6 使用示例

java 复制代码
@Service
public class OrderService {
    
    @PreAuthorize("hasRole('ADMIN')")
    public Order getOrder(Long id) {
        // 只有 ADMIN 角色可以访问
    }
    
    @PreAuthorize("hasAuthority('order:delete')")
    public void deleteOrder(Long id) {
        // 需要 order:delete 权限
    }
    
    @PreAuthorize("@orderSecurity.canView(authentication, #id)")
    public Order viewOrder(Long id) {
        // 调用自定义 Bean 的方法进行细粒度校验
    }
}

3.7 补充:SpEL 表达式扩展上下文

3.7.1 安全 SpEL 上下文

@PreAuthorize 中的 SpEL 表达式在一个特殊的上下文中执行,该上下文预定义了一些变量和函数:

变量/函数 说明 示例
authentication 当前认证对象 authentication.name
principal 当前用户主体 principal.username
hasRole(role) 检查角色 hasRole('ADMIN')
hasAuthority(auth) 检查权限 hasAuthority('order:delete')
hasAnyRole(...) 检查任一角色 hasAnyRole('USER', 'ADMIN')
hasAnyAuthority(...) 检查任一权限 hasAnyAuthority('read', 'write')
permitAll 始终允许 permitAll
denyAll 始终拒绝 denyAll
isAuthenticated() 是否已认证 isAuthenticated()
isAnonymous() 是否匿名 isAnonymous()
#变量名 方法参数引用 #id

3.7.2 SpEL 解析上下文构建

java 复制代码
// DefaultMethodSecurityExpressionHandler 的核心实现(实际源码简化版)
public class DefaultMethodSecurityExpressionHandler
        extends AbstractSecurityExpressionHandler<MethodInvocation>
        implements MethodSecurityExpressionHandler {

    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer();

    @Override
    public EvaluationContext createEvaluationContext(
            Supplier<Authentication> authentication, MethodInvocation mi) {

        // 1. 创建 MethodSecurityExpressionRoot(继承 SecurityExpressionRoot)
        MethodSecurityExpressionOperations root = createSecurityExpressionRoot(authentication, mi);
        // 内部会设置:permissionEvaluator, trustResolver, roleHierarchy, defaultRolePrefix

        // 2. 创建 MethodSecurityEvaluationContext(自动绑定方法参数)
        MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(
            root, mi, getParameterNameDiscoverer());
        // MethodSecurityEvaluationContext 构造函数内部自动:
        //   - 遍历方法参数,通过 ParameterNameDiscoverer 获取参数名
        //   - 调用 ctx.setVariable(paramName, argValue) 注册为 SpEL 变量

        // 3. 设置 BeanResolver(支持 @beanName 语法)
        ctx.setBeanResolver(getBeanResolver());

        return ctx;
    }
}

3.7.3 @beanName.method() 语法的底层处理

当 SpEL 表达式中出现 @orderSecurity.canView(authentication, #id) 时:

  1. @ 前缀表示引用 Spring Bean
  2. beanResolver 通过 BeanFactory 查找名为 orderSecurity 的 Bean
  3. 调用该 Bean 的 canView 方法
  4. 传入 authentication#id(方法参数)
java 复制代码
// BeanResolver 通过 Spring 的 BeanFactory 查找 Bean(Spring Framework 内置实现)
public class BeanFactoryResolver implements BeanResolver {
    
    private final BeanFactory beanFactory;
    
    @Override
    public Object resolve(EvaluationContext context, String beanName) {
        return this.beanFactory.getBean(beanName);
    }
}

四、自定义授权方案一:SpEL 调用自定义 Bean

4.1 实现方式

java 复制代码
@Component("orderSecurity")
public class OrderSecurity {
    
    @Autowired
    private OrderRepository orderRepository;
    
    public boolean canView(Authentication authentication, Long orderId) {
        // 1. 查询订单
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null) return false;
        
        // 2. 检查是否是订单所有者或管理员
        return order.getOwner().equals(authentication.getName()) 
            || authentication.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    }
    
    public boolean canModify(Authentication authentication, Long orderId) {
        // 更复杂的业务规则
        // ...
    }
}

4.2 使用方式

java 复制代码
@Service
public class OrderService {
    
    @PreAuthorize("@orderSecurity.canView(authentication, #id)")
    public Order viewOrder(Long id) { ... }
    
    @PreAuthorize("@orderSecurity.canModify(authentication, #id)")
    public Order updateOrder(Long id, OrderDTO dto) { ... }
}

4.3 优缺点

维度 优点 缺点
灵活性 极高,可编写任意业务逻辑 SpEL 表达式较长
可测试性 可以单独测试 Security Bean 需要额外 Bean
性能 每次调用都执行 SpEL 解析 SpEL 解析有一定开销
可维护性 集中管理权限逻辑 SpEL 字符串不易重构

五、自定义授权方案二:自定义 AuthorizationManager

5.1 方案设计

这是 Spring Security 6.x 推荐的方式,通过自定义 AuthorizationManager<MethodInvocation> 实现更类型安全的授权逻辑。

Step 1:自定义注解

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireOwner {
    String resourceParam() default "id";
}

注意:这里不需要加 @PreAuthorize("true"),因为我们会在 Step 3 中通过自定义 Advisor 直接拦截此注解,避免与默认的 PreAuthorize 拦截器产生双重拦截。

Step 2:自定义 AuthorizationManager

java 复制代码
@Component
public class ResourceOwnerAuthorizationManager 
        implements AuthorizationManager<MethodInvocation> {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Override
    public AuthorizationDecision check(
            Supplier<Authentication> authentication, MethodInvocation invocation) {
        
        // 1. 获取方法上的注解
        RequireOwner requireOwner = invocation.getMethod()
            .getAnnotation(RequireOwner.class);
        if (requireOwner == null) {
            return new AuthorizationDecision(true);
        }
        
        // 2. 获取资源 ID 参数
        String paramName = requireOwner.resourceParam();
        Object resourceId = getParameterValue(invocation, paramName);
        
        // 3. 查询资源
        Order order = orderRepository.findById((Long) resourceId).orElse(null);
        if (order == null) {
            return new AuthorizationDecision(false);
        }
        
        // 4. 检查是否是资源所有者
        Authentication auth = authentication.get();
        boolean isOwner = order.getOwner().equals(auth.getName());
        boolean isAdmin = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        
        return new AuthorizationDecision(isOwner || isAdmin);
    }
    
    private Object getParameterValue(MethodInvocation invocation, String paramName) {
        // 解析方法参数值
        //  注意:JDK 默认不保留参数名,需要在编译时加 -parameters 参数,
        //    或使用 Spring 的 ParameterNameDiscoverer(如 DefaultSecurityParameterNameDiscoverer)来获取参数名
        Parameter[] parameters = invocation.getMethod().getParameters();
        Object[] arguments = invocation.getArguments();
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].getName().equals(paramName)) {
                return arguments[i];
            }
        }
        return null;
    }
}

Step 3:注册拦截器

java 复制代码
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    
    @Autowired
    private ResourceOwnerAuthorizationManager resourceOwnerAuthManager;
    
    @Bean
    public Advisor resourceOwnerAdvisor() {
        // 定义切点:匹配带有 @RequireOwner 注解的方法
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
            "@annotation(com.example.RequireOwner)");
        
        // 创建拦截器(6.x 推荐 API,替代已废弃的 MethodSecurityInterceptor)
        AuthorizationManagerBeforeMethodInterceptor interceptor =
            new AuthorizationManagerBeforeMethodInterceptor(pointcut, resourceOwnerAuthManager);

        return new DefaultPointcutAdvisor(pointcut, interceptor);
    }
}

Step 4:使用

java 复制代码
@Service
public class OrderService {
    
    @RequireOwner(resourceParam = "id")
    public Order updateOrder(Long id, OrderDTO dto) {
        // 自动检查是否是资源所有者
    }
}

5.2 两种自定义方案对比

维度 SpEL 调用 Bean 自定义 AuthorizationManager
类型安全 弱(SpEL 字符串) 强(Java 代码)
编译期检查
IDE 支持 有限(字符串无补全) 完整(Java 代码补全)
可测试性 一般
复杂度 中等
Spring Security 推荐度 3.x/5.x 时代主流 6.x/7.x 推荐方式
适用场景 简单的权限校验 复杂的业务权限逻辑

六、@PreAuthorize 完整执行链路


七、专栏总结:Spring Security 核心知识图谱

从第一篇的"过滤器链全景图"到这最后一篇的"方法安全 AOP 全链路",我们完成了一条完整的学习路径:

java 复制代码
★ 第一层次:宏观架构
├── 01 过滤器链全景 → FilterOrderRegistration: 30+ 过滤器的"宪法"
├── 02 构建过程 → HttpSecurity Builder 模式: doBuild 三步曲
└── 03 FilterChainProxy → 总入口: 路由 + 防火墙 + 虚拟链驱动

★ 第二层次:认证体系
├── 04 认证器创建 → AuthenticationConfiguration → ProviderManager
├── 05 表单登录流程 → UsernamePasswordAuthenticationFilter 全链路
└── 06 会话与JWT → Session vs JWT 选型 + 自定义过滤器实战

★ 第三层次:授权体系
├── 07 URL 授权 → AuthorizeHttpRequestsConfigurer → AuthorizationFilter
└── 08 方法安全 → @EnableMethodSecurity → AOP → SpEL 全链路

核心组件职责速查

组件 层次 一句话职责
FilterOrderRegistration 定义层 30+ 过滤器的执行顺序"宪法"
HttpSecurity 构建层 收集 Configurer,通过 doBuild 构建过滤器链
FilterChainProxy 入口层 多链路由 + 防火墙 + 虚拟链驱动
ProviderManager 认证层 遍历 AuthenticationProvider,委托认证
DaoAuthenticationProvider 认证层 UserDetailsService 查用户 + PasswordEncoder 验密码
AuthorizationFilter 授权层(URL) 根据 authorizeHttpRequests 规则校验 URL 权限
PreAuthorizeAuthorizationManager 授权层(方法) 解析 @PreAuthorize 的 SpEL 表达式并执行权限决策
SecurityExpressionRoot 授权层(方法) hasRole/hasAuthority 等 SpEL 函数的底层实现

自定义扩展路径图

java 复制代码
简单场景:自定义 UserDetailsService → 实现 loadUserByUsername
       ↓
中等场景:自定义过滤器 → 继承 OncePerRequestFilter → addFilterBefore
       ↓
复杂场景:@PreAuthorize("@bean.method(authentication, #param)") → SpEL 调用 Bean
       ↓
生产级场景:自定义注解 + 自定义 AuthorizationManager → 注册 Advisor

专栏完结:从过滤器链全景到方法级授权,本专栏完整覆盖了 Spring Security 6.x 的核心源码与实战配置。掌握这 8 篇文章的内容,你不仅能在面试中对 Spring Security 的内部机制对答如流,更能在生产环境中从容应对各种安全需求------从传统的表单登录到前后端分离的 JWT 方案,从 URL 级别的粗粒度授权到方法级别的细粒度业务权限控制。

相关推荐
自由的裙子1 小时前
How-To: Using the N* Stack, part 4
java·jvm·oracle
逝水无殇1 小时前
C# 枚举(Enum)详解
开发语言·后端·c#
什巳2 小时前
JAVA练习275-乘积最大子数组
java·开发语言·数据结构·算法
lang201509282 小时前
Apache POI Word(docx) 实战教程:从入门到精通表格合并、图片插入与文档合并
java·开发语言·word
小牛不牛的程序员2 小时前
利用Claude Code构建自动化需求挖掘工具:从原理到实践
java·算法·自动化
树獭叔叔2 小时前
从 Search 到 Extract:AI Agent 网页工具架构拆解
后端·agent·ai编程
wear工程师2 小时前
腾讯天美一面:从 HashMap 到 Spring 事务,面试官这套连环问到底在考什么
java·面试
梨子同志2 小时前
Nacos
后端
宠友信息2 小时前
Redis 内容社区源码架构优化与即时通讯数据一致性处理
spring boot·后端·websocket·mysql·uni-app