Spring @EnableMethodSecurity註解
一、概述
使用 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()方法使用元數據為我們可能想要使用的不同授權類型創建MethodInterceptor bean。
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不允許像遺留實現中那樣使用 A 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 。我們將使用內存映射來存儲用戶:
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 測試。
與往常一樣,我們可以在 GitHub 上找到工作代碼示例。