1. 概述
使用 Spring Security,我們可以為方法(例如我們的端點)配置應用程序的身份驗證和授權。例如,如果用户在我們的域中具有身份驗證,我們可以通過在現有方法上應用限制來分析其應用程序的使用情況。
使用 @EnableGlobalMethodSecurity 註解在版本 5.6 之前一直是一種標準,但 @EnableMethodSecurity 註解在版本 5.6 之後引入了一種更靈活的方法來配置方法安全性的授權。
在本教程中,我們將看到 @EnableMethodSecurity 註解如何取代 @EnableGlobalMethodSecurity 註解。我們還將看到它們之間的差異以及一些代碼示例。
2. @EnableMethodSecurity} vs. @EnableGlobalMethodSecurity
Let’s check out how method authorization works with @EnableMethodSecurity and @EnableGlobalMethodSecurity.
2.1. @EnableMethodSecurity
With @EnableMethodSecurity, we can see the intention of Spring Security to move to a bean-based configuration for the authorization types.
Instead of a global configuration, we now have one for every type. Let’s see, for example, the Jsr250MethodSecurityConfiguration:
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MethodSecurityConfiguration {
// ...
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
AuthorizationManager jsr250AuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
}
@Autowired(required = false)
void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
}
MethodInterceptor} essentially contains an AuthorizationManager} which now delegates the responsibility of checking and returning an AuthorizationDecision} object with the final decision to the appropriate implementation, in this case, the AuthenticatedAuthorizationManager}
@Override
public AuthorizationDecision check(Supplier<Authentication>AUTHENTICATION_SUPPLIER, T object) {
boolean granted = isGranted(AUTHENTICATION_SUPPLIER.get());
return new AuthorityAuthorizationDecision(granted, this.authorities);
}
private boolean isGranted(Authentication AUTHENTICATION_SUPPLIER) {
return AUTHENTICATION_SUPPLIER != null && AUTHENTICATION_SUPPLIER.get().isAuthenticated() && isAuthorized(AUTHENTICATION_SUPPLIER);
}
private boolean isAuthorized(Authentication AUTHENTICATION_SUPPLIER) {
Set<String> authorities = AuthorityUtils.authorityListToSet(this.authorities);
for (GrantedAuthority grantedAuthority : AUTHENTICATION_SUPPLIER.get().getAuthorities()) {
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
The MethodInterceptor} throws an AccesDeniedException} if we don’t have access to a resource:
AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi);
if (decision != null && !decision.isGranted()) {
// ...
throw new AccessDeniedException("Access Denied");
}
2.2. @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity is a functional interface we need alongside @EnableWebSecurity to create our security layer and get method authorization.
Let’s create an example configuration class:
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Configuration
public class SecurityConfig {
// security beans
}
All method security implementations use a MethodInterceptor} that triggers when authorization is required . In this case, the GlobalMethodSecurityConfiguration} class is the base configuration for enabling global method security.
methodSecurityInterceptor()} creates the MethodInterceptor} bean using metadata for the different authorization types we may want to use.
Spring Security supports three in-built method security annotations:
- prePostEnabled} for Spring pre/post annotations
- securedEnabled} for Spring @Secured} annotation
- jsr250Enabled} for standard Java @RoleAllowed} annotation
Furthermore, within the methodSecurityInterceptor()}, it is also set:
- AccessDecisionManager}, which “decides” whether to grant access or not using a voting-based mechanism
- AuthenticationManager}, which we get from the security context and is responsible for authentication
- AfterInvocationManager}, which is responsible for providing the handlers for the pre/post expressions
The framework has a voting mechanism to deny or grant access to a specific method. We can check this out as an example for the Jsr250Voter}:
@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;
}
When voting, Spring Security pulls the metadata attributes from the current method, for example, our REST endpoint. Finally, it checks them against the user-granted authorities.}
We should also note the possibility for a voter not to support the voting system and abstain.
Our AccessDecisionManager} then evaluates all the responses from the available voters:
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("Access is denied");
}
3. @EnableMethodSecurity 功能
@EnableMethodSecurity 相較於之前的遺留實現,帶來了既有小幅也有大幅度的改進。
3.1. 小幅改進
所有授權類型仍然得到支持。例如,它仍然符合 JSR-250 的要求。但是,我們無需再為註解中添加 prePostEnabled,因為它現在默認設置為 true:
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
我們需要將 prePostEnabled 設置為 false,才能禁用它。
3.2. 顯著改進
GlobalMethodSecurityConfiguration 類不再使用。 Spring Security 用其分段配置和 AuthorizationManager 替代了它,這意味着我們可以定義我們的授權 Bean,而無需擴展任何基礎配置類。
值得注意的是,AuthorizationManager 接口是通用的,並且可以適應任何對象,儘管標準安全適用於 MethodInvocation:
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
總的來説,這為我們提供了細粒度的授權,使用 委託。因此,在實踐中,我們為每種類型都有一個 AuthorizationManager。當然,我們也可以自己構建。
此外,這也意味着 @EnableMethodSecurity 不允許使用 @AspectJ 註解,以及帶有 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. Custom AuthorizationManager Application
So let’s look at how to create a custom authorization manager.
Suppose we have endpoints for which we want to apply a policy. We want to authorize a user only if he has access to that policy. Otherwise, we’ll block the user.
As a first step, we define our user by adding a field to access a restricted policy:
public class SecurityUser implements UserDetails {
private String userName;
private String password;
private List<GrantedAuthority> grantedAuthorityList;
private boolean accessToRestrictedPolicy;
// getters and setters
}
Now, let’s see our authentication layer to define users in our system. For that, we’ll create a custom UserDetailService. We’ll use an in-memory map to store users:
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);
}
}
Once the user exists in our system, we want to restrict the information he can access by checking if he has access to some restricted policy.
To demonstrate, we create a Java annotation @Policy to apply on methods and a policy enum:
@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Policy {
PolicyEnum value();
}
public enum PolicyEnum {
RESTRICTED, OPEN
}
Let’s create the service to which we want to apply this policy:
@Service
public class PolicyService {
@Policy(PolicyEnum.OPEN)
public String openPolicy() {
return "Open Policy Service";
}
@Policy(PolicyEnum.RESTRICTED)
public String restrictedPolicy() {
return "Restricted Policy Service";
}
}
We can’t use an in-built authorization manager, such as the Jsr250AuthorizationManager. It wouldn’t know when and how to intercept the service policy check. So, let’s define our custom manager:
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);
}
}
When the service method is triggered, we double-check that the user has authentication. Then, we grant access if the policy is open. In case of restriction, we check if the user has access to the restricted policy.
For that, we need to define a MethodInterceptor that will be in place, for example, before the execution, but it could also be after. So let’s wrap it together with our security configuration class:
@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();
}
}
We are using MethodInterceptor. It matches our policy service pattern and uses the custom authorization manager.
Furthermore, we also need to make our AuthenticationManager aware of the custom UserDetailsService. Then, when Spring Security intercepts the service method, we can access our custom user and check the user’s policy access.
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());
}
}
所有響應都應被授權,除非調用者沒有訪問權限的服務調用。
6. 結論
在本文中,我們瞭解了 @EnableMethodSecurity 的主要功能,以及它如何取代 @EnableGlobalMethodSecurity。
我們還通過了解實現流程,學習了這兩個註解之間的差異。隨後,我們討論了 @EnableMethodSecurity 如何提供更靈活的基於 Bean 的配置。最後,我們理解了如何創建自定義授權管理器和 MVC 測試。