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 获取。