spring security
적용 후@EnableMethodSecurity
를 적용한 컨트롤러 테스트
spring security 구성
- 기존 작성된 코드에서
security
의존을 분리 시키기 위한permitAll()
처리 이후에 작성되는 코드에 대해서만
security
테스트 코드 작성SecurityFilterChain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((requests) -> requests
.requestMatchers("**.ico", "/css/**", "/js/**", "/").permitAll()
.requestMatchers("/api/v1/posts/**").permitAll()
.anyRequest().authenticated()
);
httpSecurity
.formLogin(withDefaults())
.httpBasic(withDefaults());
return httpSecurity.build();
}
}
InMemoryUserDetailService
,PasswordEncoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class ProjectConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withUsername("admin").password(passwordEncoder().encode("admin")).roles("ADMIN").build();
UserDetails user = User.withUsername("user").password(passwordEncoder().encode("user")).roles("USER").build();
UserDetails guest = User.withUsername("guest").password(passwordEncoder().encode("guest")).roles("GUEST").build();
return new InMemoryUserDetailsManager(admin, user, guest);
}
}
테스트 컨트롤러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j
@RestController
@RequestMapping(path = "/api/v1/security-test")
public class SecurityTestController {
@PreAuthorize("hasAnyRole('ADMIN','USER','GUEST')")
@GetMapping(path = "/get-all")
public String all() {
return "getAll";
}
@GetMapping(path = "/get-admin")
@PreAuthorize("hasRole('ADMIN')")
public String getAdmin() {
return "getAdmin";
}
@GetMapping(path = "/get-user")
@PreAuthorize("hasRole('USER')")
public String getUser() {
return "getUser";
}
@GetMapping(path = "/get-guest")
@PreAuthorize("hasRole('GUEST')")
public String getGuest() {
return "getGuest";
}
}
테스트 코드
SecuritySetup
security
가 적용된 통합 테스트를 진행함으로@SpringBootTest
선언.MockMvc
를 이용한 요청 테스트를 진행할 예정임으로@AutoConfigureMockMvc
선언- 테스트 코드에서 설정한
security
구성을 가져와야 함으로mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build()
선언@BeforeEach
메소드 인자에WebApplicationContext wac
를 선언하면 자동으로WebApplicationContext
주입시켜준다. admin, user, guest
테스트 대상 계정 선언 ,UserDetailService
해당 서비에서 설정한 계정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@SpringBootTest
@AutoConfigureMockMvc
public abstract class SecuritySetup {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected UserDetailsService userDetailsService;
@Autowired
protected PasswordEncoder passwordEncoder;
protected UserDetails adminDetails;
protected UserDetails userDetails;
protected UserDetails guestDetails;
@BeforeEach
void beforeEach(WebApplicationContext wac) {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
adminDetails = userDetailsService.loadUserByUsername("admin");
Assertions.assertNotNull(adminDetails);
userDetails = userDetailsService.loadUserByUsername("user");
Assertions.assertNotNull(userDetails);
guestDetails = userDetailsService.loadUserByUsername("guest");
Assertions.assertNotNull(guestDetails);
}
}
SecurityTest
- 테스트 컨트롤러에 맞게 시나리오 작성후 테스트
- 인증 정보가 없는 사용자가 접근할 때 401 에러를 뱉는지.
- 리소스에 접근할 권한이 없는 사용자가 접근할 때 403 에러를 뱉는지.
ResultActions requestHelper(String url, UserDetails userDetails)
- 인증 정보를 인자로 받아
ResultActions
을 리턴 해주는 도우미 메소드
- 인증 정보를 인자로 받아
- 테스트 컨트롤러에 맞게 시나리오 작성후 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SecurityTest extends SecuritySetup {
@Test
void authentication_test() throws Exception {
UserDetails mockUser = User.withUsername("mock_user").password("12345").roles("MOCK_USER").build();
requestHelper("/api/v1/security-test/get-all", mockUser).andExpect(status().isUnauthorized());
requestHelper("/api/v1/security-test/get-admin", mockUser).andExpect(status().isUnauthorized());
requestHelper("/api/v1/security-test/get-user", mockUser).andExpect(status().isUnauthorized());
requestHelper("/api/v1/security-test/get-guest", mockUser).andExpect(status().isUnauthorized());
}
@Test
void authorization_test() throws Exception {
requestHelper("/api/v1/security-test/get-all", adminDetails).andExpect(status().isOk());
requestHelper("/api/v1/security-test/get-all", userDetails).andExpect(status().isOk());
requestHelper("/api/v1/security-test/get-all", guestDetails).andExpect(status().isOk());
requestHelper("/api/v1/security-test/get-admin", adminDetails).andExpect(status().isOk());
requestHelper("/api/v1/security-test/get-admin", userDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-admin", guestDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-user", adminDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-user", userDetails).andExpect(status().isOk());
requestHelper("/api/v1/security-test/get-user", guestDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-guest", adminDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-guest", userDetails).andExpect(status().isForbidden());
requestHelper("/api/v1/security-test/get-guest", guestDetails).andExpect(status().isOk());
}
private ResultActions requestHelper(String url, UserDetails userDetails) throws Exception {
return mockMvc.perform(
get(url)
.with(httpBasic(userDetails.getUsername(), userDetails.getUsername()))
);
}
}
실행결과
- 발생 원인
- 이전에 설정 했던
ExceptionHandler
에서 해당 에러를 캐치해서 500코드로 뱉어내서 발생
- 이전에 설정 했던
- 문제 코드
- 디버깅으로 확인했을떄 해당 부분에서 상태 코드를 500으로 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalControllerAdvice extends ResponseEntityExceptionHandler {
// ...
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDetails<String>> globalExceptionHandler(
Exception exception, WebRequest webRequest
) {
ErrorDetails<String> errorDetails = new ErrorDetails<>(exception.getMessage(), webRequest.getDescription(false));
log.info("exception controllerAdviceResponse = {}", errorDetails);
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
// ...
}
- 해결 방법
Exception
에 잡히기 전에AccessDeniedException
를 먼저 처리 하여 반환AccessDeniedException
: 리소스에 대한 접근 권한이 없을 때 발생하는 에러
- 추가된
ExcptionHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalControllerAdvice extends ResponseEntityExceptionHandler {
// ...
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorDetails<String>> accessDeniedExceptionHandler(
AccessDeniedException exception, WebRequest webRequest
) {
ErrorDetails<String> errorDetails = new ErrorDetails<>(exception.getMessage(), webRequest.getDescription(false));
log.info("accessDeniedException controllerAdviceResponse = {}", errorDetails);
return new ResponseEntity<>(errorDetails, HttpStatus.FORBIDDEN);
}
// ...
}
- 테스트 재실행
- 리소스 접근 권한이 없을 때 반환 상태 코드가 403으로 바뀜!