[ BAEL-5804 ] Spring @EnableMethodSecurity annotation (#12980)

* enable method security with custom authorization manager

* fix: pmd violation

* fix: add user builder, clean code

* fix: test name when forbidden, user map variable
This commit is contained in:
lucaCambi77 2022-11-15 03:10:39 +01:00 committed by GitHub
parent b52f7ef200
commit 8a4e8692b9
10 changed files with 370 additions and 0 deletions

View File

@ -0,0 +1,14 @@
package com.baeldung.enablemethodsecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@SpringBootApplication
@EnableWebMvc
public class EnableMethodSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(EnableMethodSecurityApplication.class, args);
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.enablemethodsecurity.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.baeldung.enablemethodsecurity.user.SecurityUser;
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);
}
}

View File

@ -0,0 +1,48 @@
package com.baeldung.enablemethodsecurity.authorization;
import java.util.Optional;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import com.baeldung.enablemethodsecurity.services.Policy;
import com.baeldung.enablemethodsecurity.services.PolicyEnum;
import com.baeldung.enablemethodsecurity.user.SecurityUser;
public class CustomAuthorizationManager<T> implements AuthorizationManager<MethodInvocation> {
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@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);
}
}

View File

@ -0,0 +1,74 @@
package com.baeldung.enablemethodsecurity.configuration;
import static org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.baeldung.enablemethodsecurity.authentication.CustomUserDetailService;
import com.baeldung.enablemethodsecurity.authorization.CustomAuthorizationManager;
@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();
}
}

View File

@ -0,0 +1,25 @@
package com.baeldung.enablemethodsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baeldung.enablemethodsecurity.services.PolicyService;
@RestController
public class ResourceController {
private final PolicyService policyService;
public ResourceController(PolicyService policyService) {
this.policyService = policyService;
}
@GetMapping("/openPolicy")
public String openPolicy() {
return policyService.openPolicy();
}
@GetMapping("/restrictedPolicy")
public String restrictedPolicy() {
return policyService.restrictedPolicy();
}
}

View File

@ -0,0 +1,14 @@
package com.baeldung.enablemethodsecurity.services;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Policy {
PolicyEnum value();
}

View File

@ -0,0 +1,5 @@
package com.baeldung.enablemethodsecurity.services;
public enum PolicyEnum {
RESTRICTED, OPEN
}

View File

@ -0,0 +1,16 @@
package com.baeldung.enablemethodsecurity.services;
import org.springframework.stereotype.Service;
@Service
public class PolicyService {
@Policy(PolicyEnum.OPEN)
public String openPolicy() {
return "Open Policy Service";
}
@Policy(PolicyEnum.RESTRICTED)
public String restrictedPolicy() {
return "Restricted Policy Service";
}
}

View File

@ -0,0 +1,77 @@
package com.baeldung.enablemethodsecurity.user;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class SecurityUser implements UserDetails {
private String userName;
private String password;
private List<GrantedAuthority> grantedAuthorityList;
private boolean accessToRestrictedPolicy;
public static SecurityUser builder() {
return new SecurityUser();
}
public SecurityUser withAccessToRestrictedPolicy(boolean restrictedPolicy) {
this.accessToRestrictedPolicy = restrictedPolicy;
return this;
}
public boolean hasAccessToRestrictedPolicy() {
return accessToRestrictedPolicy;
}
public SecurityUser withGrantedAuthorityList(List<GrantedAuthority> grantedAuthorityList) {
this.grantedAuthorityList = grantedAuthorityList;
return this;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorityList;
}
public SecurityUser withPassword(String password) {
this.password = password;
return this;
}
@Override
public String getPassword() {
return this.password;
}
public SecurityUser withUserName(String userName) {
this.userName = userName;
return this;
}
@Override
public String getUsername() {
return this.userName;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}

View File

@ -0,0 +1,57 @@
package com.baeldung.enablemethodsecurity;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(classes = EnableMethodSecurityApplication.class)
public class EnableMethodSecurityIntegrationTest {
@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());
}
}