Spring Security๋ฅผ ์ฌ์ฉํ ๋ 400 ์๋ฌ๊ฐ ๋ฐ์ํด์ผํ ์ํฉ์ 403์๋ฌ๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค..
์ ๋ชจ๋ ์๋ฌ๊ฐ 403 Forbidden์ผ๋ก ์๋ต๋ ๊น?
ํ์ฌ ์ํฉ์ Spring Boot ์ ํ๋ฆฌ์ผ์ด์ ์ Spring Security๋ฅผ ์ ์ฉํ๊ณ API๋ฅผ ํ ์คํธ์ค ๋ถ๋ช ๋ก์ง์์ผ๋ก๋ 400 Bad Request (์: ์์ฒญ DTO ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ) ๋๋ 409 Conflict (์: ์ค๋ณต๋ ๋ฆฌ์์ค ์์ฑ ์๋) ๋ฑ์ด ๋ฐ์ํด์ผ ํ๋๋ฐ, ๋ฌ๊ธ์์ด 403 Forbidden ์๋ต๋ง ๋์์ค๋ ์ํฉ์ ๋๋ค.
๋ ๊ทธ๋ ๋ค๊ณ ์๋ฌ๊ฐ ์๋ ์ ์ ์์ฒญ์ ๋ณด๋ด๋ฉด 403์ด ์๋๋ผ 200์ด ๋์์ค๊ณ ์์ต๋๋ค.
Spring Security๋ ์ด๋ป๊ฒ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋๊ฐ?
Spring Boot์ Spring Security๊ฐ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ณผ์ ์ ์ดํด๋ณด๋ฉด
- ์ ํ๋ฆฌ์ผ์ด์
์์ ์์ธ ๋ฐ์
=> ์ปจํธ๋กค๋ฌ์ ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ(MethodArgumentNotValidException), ์๋น์ค ๋ก์ง์์์ IllegalArgumentException ๋ฑ ์์ธ ๋ฐ์ - Spring Boot์ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ
=> Spring Boot๋ ์ด๋ฌํ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก response.sendError()๋ฅผ ํธ์ถํ์ฌ WAS(์: Tomcat)์๊ฒ ์๋ฌ ์ํฉ์ ์๋ฆผ - WAS์ ์๋ฌ ํ์ด์ง ์์ฒญ
=> WAS๋ ์ด ์๋ฌ๋ฅผ ๋ฐ์ผ๋ฉด ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ์ ์ ํ ์๋ฌ ํ์ด์ง๋ฅผ ๋ค์ Spring Boot ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฒญํจ
(์ด๋ ์์ฒญํ๋ ๊ฒฝ๋ก๊ฐ ๋ฐ๋ก "/error" ) - Spring Security์ ๊ฐ์
(์ฌ๊ธฐ์ ๋ฌธ์ ๋ฐ์!!)
=> Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์์ฒญ์ ๊ฐ์ํ๋ฉฐ, ์ธ์ฆ๋์ง ์์ ์ ๊ทผ์ ์ฐจ๋จํ๋๋ฐ ๋ง์ฝ SecurityConfig์์ /error ๊ฒฝ๋ก์ ๋ํด ํน๋ณํ ์ ๊ทผ ๊ถํ์ ์ค์ ํด์ฃผ์ง ์์๋ค๋ฉด, Spring Security๋ ์ด "/error" ์์ฒญ์กฐ์ฐจ "์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์ ์ ๊ทผ"์ผ๋ก ๊ฐ์ฃผํจ - Http403ForbiddenEntryPoint ํธ์ถ
=> ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ๋ณดํธ๋ ๋ฆฌ์์ค(/error ๊ฒฝ๋ก)์ ์ ๊ทผํ๋ ค๊ณ ํ ๋, Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก Http403ForbiddenEntryPoint๋ฅผ ํธ์ถํจ
(์ด ์ปดํฌ๋ํธ๋ ํด๋ผ์ด์ธํธ์๊ฒ 403 Forbidden ์๋ต์ ๋ณด๋)
๊ฒฐ๊ตญ, ์ด๋ฐ ํ๋ฆ์ผ๋ก ์๋ ๋ฐ์ํ๋ ์์ธ(400, 409 ... ๋ฑ)๋ /error ๊ฒฝ๋ก๋ก ๋ฆฌ๋๋ ์ ๋๋ ๊ณผ์ ์์ Spring Security์ ์ํด ๊ฐ๋ก๋งํ๊ณ , ์ต์ข ์ ์ผ๋ก๋ 403 ์๋ต๋ง ๋ฐ๊ฒ ๋๋ ๊ฒ์
ํด๊ฒฐ ๊ณผ์ ๋ฐ ๊ฐ ์ปดํฌ๋ํธ์ ์ญํ
1. SecurityConfig์ "/error" ๊ฒฝ๋ก ํ์ฉ
// SecurityConfig.java
private static final String[] PERMIT_URL_ARRAY = {
// ... ๋ค๋ฅธ ๊ฒฝ๋ก๋ค ...
"/error",
// ...
};
// ...
.authorizeHttpRequests(auth -> auth
.requestMatchers(PERMIT_URL_ARRAY).permitAll()
.anyRequest().authenticated()
)
// ...
๊ฐ์ฅ ๋จผ์ "/error" ๊ฒฝ๋ก์ ๋ํด ๋ชจ๋ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ์ ์๋๋ก permitAll() ์ค์ ํ์์
์ด๋ ๊ฒ ํ๋ฉด Spring Security๊ฐ "/error" ๊ฒฝ๋ก ์์ฒด์ ๋ํ ์ ๊ทผ์ ๋ง์ง ์์ผ๋ฏ๋ก, Tomcat์ด ์๋ฌ ํ์ด์ง๋ฅผ ์์ฒญํ์ ๋ 403 ๋์ Spring Boot์ BasicErrorController๊ฐ ์๋ต์ ์ฒ๋ฆฌํ ์ ์๊ฒ๋จ
BasicErrorController๋ ๋ฐ์ํ ์์ธ์ ๋ฐ๋ผ ์ ์ ํ HTTP ์ํ ์ฝ๋(ex. 400, 500)์ ๊ธฐ๋ณธ ์ค๋ฅ ๋ฉ์์ง๋ฅผ JSON ํํ๋ก ๋ฐํํด์ค
=> ํ์ง๋ง BasicErrorController๊ฐ ๋ฐํํ๋ ์๋ต ํ์์ด ๊ธฐ๋ํ๋ ํ์คํ๋ API ์ค๋ฅ ์๋ต ํ์์ด ์๋ ์ ์๊ณ , ํนํ Spring Security๊ฐ ์ง์ ์ฒ๋ฆฌํ๋ ์ธ์ฆ(Authentication) ๋ฐ ์ธ๊ฐ(Access Denied) ๊ด๋ จ ์์ธ์ ๋ํด์๋ ์ฌ์ ํ ๊ธฐ๋ณธ ๋์๋๋ก HTML ํ์ด์ง๋ฅผ ๋ฐํํ๋ ค๊ณ ํ๊ฑฐ๋, ์์น ์๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋ ์ ์์!!!
(AuthenticationEntryPoint์ AccessDeniedHandler๋ฅผ ์ปค์คํฐ๋ง์ด์งํ๋ ์ด์ ์๐↔๏ธ)
2. customAuthenticationEntryPoint ์ customAccessDeniedHandler ๋ฑ๋ก
// SecurityConfig.java
// ...
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(customAuthenticationEntryPoint()) // ์ธ์ฆ ์คํจ ์ ์ฒ๋ฆฌ
.accessDeniedHandler(customAccessDeniedHandler()) // ์ธ๊ฐ ์คํจ ์ ์ฒ๋ฆฌ
)
// ...
@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return (request, response, authException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", "์ธ์ฆ์ด ํ์ํ๊ฑฐ๋ ์ ํจํ์ง ์์ ์ธ์ฆ ์ ๋ณด์
๋๋ค: " + authException.getMessage());
new ObjectMapper().writeValue(response.getWriter(), body);
};
}
@Bean
public AccessDeniedHandler customAccessDeniedHandler() {
return (request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_FORBIDDEN);
body.put("error", "Forbidden");
body.put("message", "ํด๋น ๋ฆฌ์์ค์ ์ ๊ทผํ ๊ถํ์ด ์์ต๋๋ค: " + accessDeniedException.getMessage());
new ObjectMapper().writeValue(response.getWriter(), body);
};
}
- AuthenticationEntryPoint: ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์(ex, ์ ํจํ์ง ์์ JWT ํ ํฐ, ํ ํฐ ์์)๊ฐ ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ค๊ณ ํ ๋ ํธ์ถ๋จ
=> customAuthenticationEntryPoint์์๋ ์ด๋ฅผ ๊ฐ๋ก์ฑ์ ์ผ๊ด๋ JSON ํ์์ 401 Unauthorized ์๋ต์ ๋ณด๋ด๋๋ก ์ค์
- AccessDeniedHandler: ์ธ์ฆ์ ๋์์ง๋ง(์ ํจํ ์ฌ์ฉ์๋ ๋ง์ง๋ง) ํด๋น ๋ฆฌ์์ค์ ์ ๊ทผํ ํน์ ๊ถํ์ด ์๋ ๊ฒฝ์ฐ ํธ์ถ๋จ
=> customAccessDeniedHandler์์๋ ์ด๋ฅผ ๊ฐ๋ก์ฑ์ ์ผ๊ด๋ JSON ํ์์ 403 Forbidden ์๋ต์ ๋ณด๋ด๋๋ก ์ค์
์ด ๋ handler๋ฅผ SecurityConfig์ exceptionHandling()์ ๋ฑ๋กํ๋ฉด Spring Security ํํฐ ์ฒด์ธ ๋ด์์ ๋ฐ์ํ๋ ์ธ์ฆ/์ธ๊ฐ ๊ด๋ จ ์์ธ๋ ์ ์ํ ๋ฐฉ์๋๋ก ๊น๋ํ๊ฒ JSON ์๋ต์ผ๋ก ์ฒ๋ฆฌ๋จ
3. GlobalExceptionHandler ์ถ๊ฐ (@RestControllerAdvice)
// ์์: GlobalExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// ๊ณตํต ์๋ฌ ์๋ต ์์ฑ ๋ฉ์๋
private ResponseEntity<Map<String, Object>> createErrorResponse(HttpStatus status, String errorCode, String message, Map<String, String> validationErrors) {
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("status", status.value());
responseBody.put("error", errorCode);
responseBody.put("message", message);
if (validationErrors != null && !validationErrors.isEmpty()) {
responseBody.put("errors", validationErrors);
}
return ResponseEntity.status(status).body(responseBody);
}
// DTO ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์ (jakarta.validation.Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // ๊ธฐ๋ณธ์ ์ผ๋ก 400 ์๋ต
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
log.warn("Validation failed: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return createErrorResponse(HttpStatus.BAD_REQUEST, "ValidationFailed", "์
๋ ฅ๊ฐ ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ์ต๋๋ค.", errors);
}
// ์๋น์ค ๋ก์ง ๋ฑ์์ ๋ช
์์ ์ผ๋ก ์๋ชป๋ ์ธ์๋ฅผ ์ ๋ฌํ์ ๋
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 400 ์๋ต
public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex) {
log.warn("Illegal argument: {}", ex.getMessage(), ex);
return createErrorResponse(HttpStatus.BAD_REQUEST, "IllegalArgument", ex.getMessage(), null);
}
// ๋ถ์ ์ ํ ์ํ์์ ๋ฉ์๋๊ฐ ํธ์ถ๋์์ ๋ (์: ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์์ฒญ ์ฌ์๋)
@ExceptionHandler(IllegalStateException.class)
@ResponseStatus(HttpStatus.CONFLICT) // 409 ์๋ต (๋๋ ์ํฉ์ ๋ฐ๋ผ 400)
public ResponseEntity<Map<String, Object>> handleIllegalStateException(IllegalStateException ex) {
log.warn("Illegal state: {}", ex.getMessage(), ex);
return createErrorResponse(HttpStatus.CONFLICT, "IllegalState", ex.getMessage(), null);
}
// ... ๊ธฐํ ์์ธ ์ฒ๋ฆฌ ๋ฉ์๋
// ์์์ ๋ช
์์ ์ผ๋ก ์ฒ๋ฆฌํ์ง ์์ ๋ชจ๋ ์์ธ
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500 ์๋ต
public ResponseEntity<Map<String, Object>> handleAllUncaughtException(Exception ex) {
log.error("Unhandled exception occurred: {}", ex.getMessage(), ex);
return createErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "InternalServerError", "์๋ฒ ๋ด๋ถ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๊ด๋ฆฌ์์๊ฒ ๋ฌธ์ํด์ฃผ์ธ์.", null);
}
}
GlobalExceptionHandler๋ @RestControllerAdvice ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฐ์์ ๋ฐ์ํ๋ ์์ธ๋ค์ ํ ๊ณณ์์ ์ฒ๋ฆฌํ๋ ์ญํ ์ํจ
=> Spring Security์ ์ธ์ฆ/์ธ๊ฐ ํํฐ๋ฅผ ํต๊ณผํ ํ, ์ปจํธ๋กค๋ฌ๋ ์๋น์ค ๋ ์ด์ด์์ ๋ฐ์ํ๋ ์์ธ๋ค(์: MethodArgumentNotValidException (DTO ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ), IllegalArgumentException, IllegalStateException ๋ฑ ๋น์ฆ๋์ค ๋ก์ง ๊ด๋ จ ์์ธ)์ ์ฒ๋ฆฌํจ
@ExceptionHandler ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด ํน์ ์์ธ ํ์ ๋ณ๋ก ์ฒ๋ฆฌ ๋ก์ง์ ์ ์ํ ์๋ ์์
/error ๊ฒฝ๋ก๋ฅผ ์ด๊ณ , Spring Security ์์ธ ์ฒ๋ฆฌ๊ธฐ๋ฅผ ์ปค์คํ
ํ๋๋ผ๋, ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋ ๋ด์์ ๋ฐ์ํ๋ ์ผ๋ฐ์ ์ธ ์์ธ๋ค์ ์ฌ์ ํ BasicErrorController์ ์ํด ๊ธฐ๋ณธ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋ ์ ์๋๋ฐ GlobalExceptionHandler๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ฌํ ์์ธ๋ค๊น์ง๋ ๋ฏธ๋ฆฌ ์ ์ํ ์ผ๊ด๋ JSON ์ค๋ฅ ํ์์ผ๋ก ์๋ตํ ์ ์๊ฒ๋จ
๊ฒฐ๋ก
์ ๋ฆฌ๋ฅผ ํด๋ณด๋ฉด
- ์ด๋ค ์ข
๋ฅ์ ์์ธ๋ ๋ฐ์ํ๋ฉด, Spring Boot๋ /error ๊ฒฝ๋ก๋ก ๋ด๋ถ ๋ฆฌ๋๋ ์
์ ์๋ํจ
- SecurityConfig์์ "/error" ๊ฒฝ๋ก๋ฅผ permitAll()๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์, ์ด /error ์์ฒญ์ Spring Security์ ์ํด ์ฐจ๋จ๋์ง ์์
- ์ด์ ์์ธ์ ์ข ๋ฅ์ ๋ฐ๋ผ ์ฒ๋ฆฌ ์ฃผ์ฒด๊ฐ ๋ฌ๋ผ์ง
- Spring Security ๊ด๋ จ ์์ธ (์ธ์ฆ ์คํจ, ์ธ๊ฐ ์คํจ)๋ HttpSecurity์ ๋ฑ๋ก๋ customAuthenticationEntryPoint ๋๋ customAccessDeniedHandler๊ฐ ๋์ํ์ฌ ๋ฏธ๋ฆฌ ์ ์ํ 401 ๋๋ 403 JSON ์๋ต์ ๋ฐํํจ
- ์ ํ๋ฆฌ์ผ์ด์
๋ ๋ฒจ ์์ธ (ex. DTO ์ ํจ์ฑ ๊ฒ์ฌ, ๋น์ฆ๋์ค ๋ก์ง ์์ธ ๋ฑ)๋ /error ๊ฒฝ๋ก๋ฅผ ํตํด BasicErrorController๋ก ์ ๋ฌ๋๋ ค ํ์ง๋ง, ๊ทธ ์ ์ @RestControllerAdvice๋ก ์ ์ธ๋ GlobalExceptionHandler๊ฐ ๋จผ์ ๊ฐ๋ก์ฑ
=> GlobalExceptionHandler ๋ด์ ์ ์ ํ @ExceptionHandler ๋ฉ์๋๊ฐ ํด๋น ์์ธ๋ฅผ ์ฒ๋ฆฌํ์ฌ, ์ฐ๋ฆฌ๊ฐ ์ ์ํ HTTP ์ํ ์ฝ๋(400, 409, 500 ๋ฑ)์ JSON ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋ฐํํจ
์ด๋ ๊ฒ ํด๋ผ์ด์ธํธ๋ ์ด๋ค ์ข
๋ฅ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ์ผ๊ด๋๊ณ ๋ช
ํํ ์ค๋ฅ ์๋ต์ ๋ฐ์ ์ ์๊ฒ ๋จ
=> ๊ธฐ์กด์ ๋ชจ๋ ์ค๋ฅ๊ฐ 403์ผ๋ก ๋ฎ์ด์ฐ์ด๋ ์๋ฌ๊ฐ ํด๊ฒฐ๋จ!!
'๐ซ Backend > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring์์ ๊ฐ์ฒด๋ฅผ Bean์ผ๋ก ๊ด๋ฆฌํ๋ ์ด์ (0) | 2025.06.23 |
---|---|
application.yml์ ์ด๋ป๊ฒ ๊ด๋ฆฌํด์ผํ ๊น? (1) | 2025.06.21 |
[Spring Boot] HTTPS(NginX) ๋ฐฐํฌ ํ๊ฒฝ์์ Swagger ์๋ฌ (0) | 2025.04.18 |
[Spring Boot] AWS Lambda์ ํต์ ํ๊ธฐ (0) | 2025.03.30 |
NginX๋ฅผ ํตํด Spring Boot ์๋ฒ์ HTTPS ์ ์ฉํ๊ธฐ (0) | 2025.03.24 |