拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 Spring @EnableMethodSecurity注解

Spring @EnableMethodSecurity注解

白鹭 - 2022-11-29 2207 0 2

一、概述

使用Spring Security,我们可以为端点等方法配置应用程序的身份验证和授权。例如,如果用户在我们的域上进行了身份验证,我们可以通过对现有方法应用限制来分析他对应用程序的使用情况。

使用@EnableGlobalMethodSecurity注释一直是标准,直到5.6 版,当时@EnableMethodSecurity引入了一种更灵活的配置方法安全授权的方法。

在本教程中,我们将看到@EnableMethodSecurity如何替换旧注解。我们还将看到它的前身和一些代码示例之间的区别。

2.@EnableMethodSecurity@EnableGlobalMethodSecurity

如果我们首先检查方法授权如何与@EnableGlobalMethodSecurity工作,我们可以了解更多关于这个主题的信息。

2.1.@EnableGlobalMethodSecurity

[@EnableGlobalMethodSecurity](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html)是一个功能接口,我们需要与@EnableWebSecurity一起创建我们的安全层并获得方法授权。

让我们创建一个示例配置类:

@EnableWebSecurity
 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
 @Configuration
 public class SecurityConfig {
 // security beans
 }

所有方法安全实现都使用一个在需要授权时触发的在这种情况下,类是启用全局方法安全性的基本配置。MethodInterceptor .GlobalMethodSecurityConfiguration

methodSecurityInterceptor()方法使用元数据为我们可能想要使用的不同授权类型创建MethodInterceptorbean。

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

  • prePostEnabled用于Spring 前/后注释

  • securedEnabled用于Spring@Secured注解

  • jsr250Enabled用于标准Java@RoleAllowed注释

此外,在methodSecurityInterceptor()中,还设置了:

  • AccessDecisionManager,它使用基于投票的机制“决定”是否授予访问权限

  • [AuthenticationManager](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authentication/AuthenticationManager.html) ,我们从安全上下文中获取并负责身份验证

  • AfterInvocationManager,负责为前/后表达式提供处理程序

该框架有一个投票机制来拒绝或授予对特定方法的访问权限。我们可以将其作为[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;
 // Attempt to find a matching granted authority
 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"));
 }

如果我们想定制我们的beans,我们可以扩展GlobalMethodSecurityConfiguration.例如,我们可能想要一个自定义的安全表达式,而不是Spring Security 内置的Spring EL或者我们可能想要制作我们的自定义安全投票器。

2.2.@EnableMethodSecurity

使用@EnableMethodSecurity,我们可以看到Spring Security 将授权类型转移到基于bean 的配置的意图。

我们现在没有全局配置,而是每种类型都有一个。例如,让我们看看Jsr250MethodSecurityConfiguration

@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,它现在将检查和返回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会抛出AccesDeniedException

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

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不允许像遗留实现中那样使用AAspectJ方法拦截器进行@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我们将使用内存映射来存储用户:

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(map.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()
 .disable()
 .authorizeRequests()
 .anyRequest()
 .authenticated()
 .and()
 .sessionManagement()
 .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
 return http.build();
 }
 @Bean
 public BCryptPasswordEncoder bCryptPasswordEncoder() {
 return new BCryptPasswordEncoder();
 }
 }

我们正在使用AuthorizationManagerBeforeMethodInterceptor它符合我们的策略服务模式并使用自定义授权管理器。

此外,我们还需要让我们的AuthenticationManager知道自定义的UserDetailsService然后,当Spring Security 拦截服务方法时,我们可以访问我们的自定义用户并检查用户的策略访问。

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());
 }
 }

所有响应都应经过授权,但用户调用他无权访问受限策略的服务的响应除外。

六,结论

在本文中,我们了解了@EnableMethodSecurity的主要特性以及它如何替代@EnableGlobalMethodSecurity.

我们还通过执行流程了解了这些注释之间的区别.然后,我们讨论了@EnableMethodSecurity如何通过基于bean 的配置提供更大的灵活性.最后,我们了解了如何创建自定义授权管理器和MVC 测试。


标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *