1. 概述

Spring Security 中,我们可以为接口等方法配置应用的认证和授权。例如,当用户完成域认证后,我们可以通过在方法上应用限制来控制其应用访问权限。

直到 5.6 版本,使用 *[@EnableGlobalMethodSecurity](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#jc-enable-global-method-security)* 注解一直是标准做法。后来引入的 *[@EnableMethodSecurity](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#_enablemethodsecurity)*方法安全提供了更灵活的配置方式。

本文将深入探讨 @EnableMethodSecurity 如何替代 @EnableGlobalMethodSecurity,分析两者差异并通过代码示例展示实际应用。

2. @EnableMethodSecurity vs @EnableGlobalMethodSecurity

2.1. @EnableMethodSecurity

@EnableMethodSecurity 体现了 Spring Security 向基于 Bean 的配置转型的意图。现在每种授权类型都有独立的配置,而非全局统一。以 [*Jsr250MethodSecurityConfiguration*](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/access/annotation/Jsr250SecurityConfig.html) 为例:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MethodSecurityConfiguration {
    // ...
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    Advisor jsr250AuthorizationMethodInterceptor() {
        return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
    }

    @Autowired(required = false)
    void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
        this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
    }
}

核心机制:*MethodInterceptor* 本质包含一个 [*AuthorizationManager*](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authorization/AuthorizationManager.html),它将检查权限并返回最终决策的 *AuthorizationDecision* 对象的责任委托给特定实现(此处是 *AuthenticatedAuthorizationManager*

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
    boolean granted = isGranted(authentication.get());
    return new AuthorityAuthorizationDecision(granted, this.authorities);
}

private boolean isGranted(Authentication authentication) {
    return authentication != null && authentication.isAuthenticated() && isAuthorized(authentication);
}

private boolean isAuthorized(Authentication authentication) {
    Set<String> authorities = AuthorityUtils.authorityListToSet(this.authorities);
    for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
        if (authorities.contains(grantedAuthority.getAuthority())) {
            return true;
        }
    }
    return false;
}

当无权访问资源时,MethodInterceptor 会抛出 AccessDeniedException

AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi);
if (decision != null && !decision.isGranted()) {
    // ...
    throw new AccessDeniedException("Access Denied");
}

2.2. @EnableGlobalMethodSecurity

*[@EnableGlobalMethodSecurity](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html)* 是一个函数式接口,需与 *[@EnableWebSecurity](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.html)* 配合使用来构建安全层和方法授权。

典型配置类示例:

@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Configuration
public class SecurityConfig {
    // 安全相关 Bean
}

所有方法安全实现都使用 [*MethodInterceptor*](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/aopalliance/intercept/MethodInterceptor.html) 在需要授权时触发。此时 [*GlobalMethodSecurityConfiguration*](https://docs.spring.io/spring-security/site/docs/6.0.0-M3/api/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.html) 类是启用全局方法安全的基础配置。

核心机制:[*methodSecurityInterceptor()*](https://docs.spring.io/spring-security/site/docs/6.0.0-M3/api/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.html#methodSecurityInterceptor\(org.springframework.security.access.method.MethodSecurityMetadataSource\)) 方法使用不同授权类型的元数据创建 MethodInterceptor Bean

Spring Security 支持三种内置方法安全注解:

  • prePostEnabled:Spring 前置/后置注解
  • securedEnabled:Spring @Secured 注解
  • jsr250Enabled:标准 Java @RoleAllowed 注解

methodSecurityInterceptor() 中还会设置:

  • [*AccessDecisionManager*](https://docs.spring.io/spring-security/site/docs/6.0.0-M3/api/org/springframework/security/access/AccessDecisionManager.html):通过投票机制决定是否授权
  • *[AuthenticationManager](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authentication/AuthenticationManager.html)*:从安全上下文获取,负责认证
  • [*AfterInvocationManager*](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/access/intercept/AfterInvocationManager.html):提供前置/后置表达式的处理器

框架通过投票机制决定是否允许访问特定方法。以 *[Jsr250Voter](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/access/annotation/Jsr250Voter.html)* 为例:

@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> definition) {
    boolean jsr250AttributeFound = false;
    for (ConfigAttribute attribute : definition) {
        if (Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE.equals(attribute)) {
            return ACCESS_GRANTED;
        }
        if (Jsr250SecurityConfig.DENY_ALL_ATTRIBUTE.equals(attribute)) {
            return ACCESS_DENIED;
        }
        if (supports(attribute)) {
            jsr250AttributeFound = true;
            // 尝试匹配授权权限
            for (GrantedAuthority authority : authentication.getAuthorities()) {
                if (attribute.getAttribute().equals(authority.getAuthority())) {
                    return ACCESS_GRANTED;
                }
            }
        }
    }
    return jsr250AttributeFound ? ACCESS_DENIED : ACCESS_ABSTAIN;
}

投票时,Spring Security 从当前方法(如 REST 接口)提取元数据属性,然后与用户权限进行比对

⚠️ 注意:投票器可能不支持投票系统而选择弃权。

AccessDecisionManager 评估所有投票器的响应:

for (AccessDecisionVoter voter : getDecisionVoters()) {
    int result = voter.vote(authentication, object, configAttributes);
    switch (result) {
        case AccessDecisionVoter.ACCESS_GRANTED:
            return;
        case AccessDecisionVoter.ACCESS_DENIED:
            deny++;
            break;
        default:
            break;
    }
}
if (deny > 0) {
    throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}

如需自定义 Bean,可继承 GlobalMethodSecurityConfiguration 类。例如创建自定义安全表达式替代 Spring EL,或实现自定义安全投票器

3. @EnableMethodSecurity 新特性

相比旧版实现,@EnableMethodSecurity 带来了多项改进。

3.1. 小幅改进

所有授权类型仍被支持,例如继续兼容 JSR-250。但无需在注解中添加 prePostEnabled,因其默认值为 true

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)

如需禁用,需显式设置 prePostEnabled = false

3.2. 重大改进

GlobalMethodSecurityConfiguration 类已不再使用。Spring Security 用分段配置和 *[AuthorizationManager](https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#_the_authorizationmanager)* 替代它,这意味着无需继承基础配置类即可定义授权 Bean

值得注意的是,AuthorizationManager 接口是泛型的,可适配任何对象(尽管标准安全应用于 MethodInvocation):

AuthorizationDecision check(Supplier<Authentication> authentication, T object);

整体上,这通过委托模式提供了细粒度的授权控制。实践中每种类型都有独立的 AuthorizationManager,当然也可以自定义。

此外,@EnableMethodSecurity 不再允许像旧版那样使用 @AspectJ 注解和 AspectJ 方法拦截器:

public final class AspectJMethodSecurityInterceptor extends MethodSecurityInterceptor {
    public Object invoke(JoinPoint jp) throws Throwable {
        return super.invoke(new MethodInvocationAdapter(jp));
    }
    // ...
}

但依然提供完整的 AOP 支持。例如前文讨论的 Jsr250MethodSecurityConfiguration 使用的拦截器:

public final class AuthorizationManagerBeforeMethodInterceptor
  implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
    // ...
    public AuthorizationManagerBeforeMethodInterceptor(
      Pointcut pointcut, AuthorizationManager<MethodInvocation> authorizationManager) {
        Assert.notNull(pointcut, "pointcut cannot be null");
        Assert.notNull(authorizationManager, "authorizationManager cannot be null");
        this.pointcut = pointcut;
        this.authorizationManager = authorizationManager;
    }
    
    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        attemptAuthorization(mi);
        return mi.proceed();
    }
}

4. 自定义 AuthorizationManager 实战

下面演示如何创建自定义授权管理器。

假设有需要应用策略的接口,仅当用户有权访问该策略时才授权,否则阻止访问。

第一步:扩展用户定义,添加受限策略访问字段:

public class SecurityUser implements UserDetails {
    private String userName;
    private String password;
    private List<GrantedAuthority> grantedAuthorityList;
    private boolean accessToRestrictedPolicy;

    // getters and setters
}

第二步:创建认证层定义系统用户。自定义 [*UserDetailService*](https://docs.spring.io/spring-security/site/docs/5.7.5/api/org/springframework/security/core/userdetails/UserDetailsService.html),使用内存 Map 存储用户:

public class CustomUserDetailService implements UserDetailsService {
    private final Map<String, SecurityUser> userMap = new HashMap<>();

    public CustomUserDetailService(BCryptPasswordEncoder bCryptPasswordEncoder) {
        userMap.put("user", createUser("user", bCryptPasswordEncoder.encode("userPass"), false, "USER"));
        userMap.put("admin", createUser("admin", bCryptPasswordEncoder.encode("adminPass"), true, "ADMIN", "USER"));
    }

    @Override
    public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
        return Optional.ofNullable(userMap.get(username))
          .orElseThrow(() -> new UsernameNotFoundException("User " + username + " does not exists"));
    }

    private SecurityUser createUser(String userName, String password, boolean withRestrictedPolicy, String... role) {
        return SecurityUser.builder().withUserName(userName)
          .withPassword(password)
          .withGrantedAuthorityList(Arrays.stream(role)
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList()))
          .withAccessToRestrictedPolicy(withRestrictedPolicy);
    }
}

第三步:创建 Java 注解 @Policy 和策略枚举:

@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Policy {
    PolicyEnum value();
}
public enum PolicyEnum {
    RESTRICTED, OPEN
}

第四步:创建应用策略的服务:

@Service
public class PolicyService {
    @Policy(PolicyEnum.OPEN)
    public String openPolicy() {
        return "Open Policy Service";
    }

    @Policy(PolicyEnum.RESTRICTED)
    public String restrictedPolicy() {
        return "Restricted Policy Service";
    }
}

第五步:自定义授权管理器(内置的 Jsr250AuthorizationManager 无法处理策略检查):

public class CustomAuthorizationManager<T> implements AuthorizationManager<MethodInvocation> {
    ...
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
        if (hasAuthentication(authentication.get())) {
            Policy policyAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), Policy.class);
            SecurityUser user = (SecurityUser) authentication.get().getPrincipal();
            return new AuthorizationDecision(Optional.ofNullable(policyAnnotation)
              .map(Policy::value).filter(policy -> policy == PolicyEnum.OPEN 
                || (policy == PolicyEnum.RESTRICTED && user.hasAccessToRestrictedPolicy())).isPresent());
        }
        return new AuthorizationDecision(false);
    }

    private boolean hasAuthentication(Authentication authentication) {
        return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated();
    }

    private boolean isNotAnonymous(Authentication authentication) {
        return !this.trustResolver.isAnonymous(authentication);
    }
}

第六步:定义 MethodInterceptor(可在方法执行前/后拦截),整合到安全配置:

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager(
      HttpSecurity httpSecurity, UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
        return new CustomUserDetailService(bCryptPasswordEncoder);
    }

    @Bean
    public AuthorizationManager<MethodInvocation> authorizationManager() {
        return new CustomAuthorizationManager<>();
    }

    @Bean
    @Role(ROLE_INFRASTRUCTURE)
    public Advisor authorizationManagerBeforeMethodInterception(AuthorizationManager<MethodInvocation> authorizationManager) {
        JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
        pattern.setPattern("com.baeldung.enablemethodsecurity.services.*");
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, authorizationManager);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
          .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> 
            authorizationManagerRequestMatcherRegistry.anyRequest().authenticated())
          .sessionManagement(httpSecuritySessionManagementConfigurer -> 
            httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

✅ 关键点:

  • 使用 AuthorizationManagerBeforeMethodInterceptor 匹配策略服务模式
  • AuthenticationManager 感知自定义 UserDetailsService
  • 拦截服务方法时访问自定义用户并检查策略权限

5. 测试验证

创建 REST 控制器:

@RestController
public class ResourceController {
    // ...
    @GetMapping("/openPolicy")
    public String openPolicy() {
        return policyService.openPolicy();
    }

    @GetMapping("/restrictedPolicy")
    public String restrictedPolicy() {
        return policyService.restrictedPolicy();
    }
}

使用 Spring Boot Test 进行方法安全测试:

@SpringBootTest(classes = EnableMethodSecurityApplication.class)
public class EnableMethodSecurityTest {
    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(context)
          .apply(springSecurity())
          .build();
    }

    @Test
    @WithUserDetails(value = "admin")
    public void whenAdminAccessOpenEndpoint_thenOk() throws Exception {
        mvc.perform(get("/openPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails(value = "admin")
    public void whenAdminAccessRestrictedEndpoint_thenOk() throws Exception {
        mvc.perform(get("/restrictedPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails()
    public void whenUserAccessOpenEndpoint_thenOk() throws Exception {
        mvc.perform(get("/openPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails()
    public void whenUserAccessRestrictedEndpoint_thenIsForbidden() throws Exception {
        mvc.perform(get("/restrictedPolicy"))
          .andExpect(status().isForbidden());
    }
}

测试结果预期:

  • ✅ 管理员访问开放/受限接口均成功
  • ✅ 普通用户访问开放接口成功
  • ❌ 普通用户访问受限接口返回 403

6. 总结

本文深入剖析了 @EnableMethodSecurity 的核心特性及其替代 @EnableGlobalMethodSecurity 的实现方式。通过分析实现流程,我们理解了两种注解的关键差异,并展示了 @EnableMethodSecurity 如何通过基于 Bean 的配置提供更高灵活性。最后通过自定义授权管理器和 MVC 测试的实战案例,巩固了理论知识。

完整代码示例可在 GitHub 获取。


原始标题:Spring @EnableMethodSecurity Annotation