Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package site.icebang.common.exception;

public class DuplicateDataException extends RuntimeException {

public DuplicateDataException() {
super();
}

public DuplicateDataException(String message) {
super(message);
}

public DuplicateDataException(String message, Throwable cause) {
super(message, cause);
}

public DuplicateDataException(Throwable cause) {
super(cause);
}

protected DuplicateDataException(
String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import lombok.RequiredArgsConstructor;

import site.icebang.common.exception.DuplicateDataException;
import site.icebang.common.utils.RandomPasswordGenerator;
import site.icebang.domain.auth.dto.RegisterDto;
import site.icebang.domain.auth.mapper.AuthMapper;
Expand All @@ -23,7 +24,7 @@ public class AuthService {

public void registerUser(RegisterDto registerDto) {
if (authMapper.existsByEmail(registerDto.getEmail())) {
throw new IllegalArgumentException("이미 가입된 이메일입니다.");
throw new DuplicateDataException("이미 가입된 이메일입니다.");
}
String randomPassword = passwordGenerator.generate();
String hashedPassword = passwordEncoder.encode(randomPassword);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;

import site.icebang.domain.auth.service.AuthCredentialAdapter;
import site.icebang.global.config.security.endpoints.SecurityEndpoints;
import site.icebang.global.handler.exception.RestAccessDeniedHandler;
import site.icebang.global.handler.exception.RestAuthenticationEntryPoint;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final Environment environment;
private final AuthCredentialAdapter userDetailsService;
private final ObjectMapper objectMapper;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final RestAccessDeniedHandler restAccessDeniedHandler;

@Bean
public AuthenticationProvider authenticationProvider() {
Expand Down Expand Up @@ -97,6 +104,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.logout(
logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login").permitAll())
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(
ex ->
ex.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restAccessDeniedHandler))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package site.icebang.global.handler.exception;

import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import site.icebang.common.dto.ApiResponse;
import site.icebang.common.exception.DuplicateDataException;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<String> handleValidation(MethodArgumentNotValidException ex) {
String detail = ex.getBindingResult().toString();
return ApiResponse.error("Validation failed: " + detail, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<String> handleGeneric(Exception ex) {
return ApiResponse.error(
"Internal error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiResponse<String> handleNotFound(NoResourceFoundException ex) {
return ApiResponse.error("Notfound: " + ex.getMessage(), HttpStatus.NOT_FOUND);
}

@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ApiResponse<String> handleAuthentication(AuthenticationException ex) {
return ApiResponse.error("Authentication failed: " + ex.getMessage(), HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ApiResponse<String> handleAccessDenied(AccessDeniedException ex) {
return ApiResponse.error("Access denied: " + ex.getMessage(), HttpStatus.FORBIDDEN);
}

@ExceptionHandler(DuplicateDataException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiResponse<String> handleDuplicateData(DuplicateDataException ex) {
return ApiResponse.error("Duplicate: " + ex.getMessage(), HttpStatus.CONFLICT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package site.icebang.global.handler.exception;

import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

import site.icebang.common.dto.ApiResponse;

@Component
@RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;

@Override
public void handle(
HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
throws IOException {
ApiResponse<String> body = ApiResponse.error("Access denied", HttpStatus.FORBIDDEN);

response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(body));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package site.icebang.global.handler.exception;

import java.io.IOException;

import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

import site.icebang.common.dto.ApiResponse;

@Component
@RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;

@Override
public void commence(
HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
throws IOException {
ApiResponse<String> body =
ApiResponse.error("Authentication required", HttpStatus.UNAUTHORIZED);

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(body));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.*;
Expand Down Expand Up @@ -116,52 +115,6 @@ void completeUserRegistrationFlow() throws Exception {
logCompletion("ERP 사용자 등록 플로우");
}

@Disabled
@DisplayName("로그인 없이 리소스 접근 시 모든 요청 차단")
void accessResourcesWithoutLogin_shouldFailForAll() {
logStep(1, "인증 없이 조직 목록 조회 시도");

// 1. 로그인 없이 조직 목록 조회 시도
ResponseEntity<Map> orgResponse =
restTemplate.getForEntity(getV0ApiUrl("/organizations"), Map.class);

assertThat(orgResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN);
logSuccess("미인증 조직 조회 차단 확인");

logStep(2, "인증 없이 조직 옵션 조회 시도");

// 2. 로그인 없이 조직 옵션 조회 시도
ResponseEntity<Map> optResponse =
restTemplate.getForEntity(getV0ApiUrl("/organizations/1/options"), Map.class);

assertThat(optResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN);
logSuccess("미인증 옵션 조회 차단 확인");

logStep(3, "인증 없이 회원가입 시도");

// 3. 로그인 없이 회원가입 시도
Map<String, Object> registerRequest = new HashMap<>();
registerRequest.put("name", "테스트사용자");
registerRequest.put("email", "test@example.com");
registerRequest.put("orgId", 1);
registerRequest.put("deptId", 2);
registerRequest.put("positionId", 5);
registerRequest.put("roleIds", Arrays.asList(6));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(registerRequest, headers);

ResponseEntity<Map> regResponse =
restTemplate.postForEntity(getV0ApiUrl("/auth/register"), entity, Map.class);

assertThat(regResponse.getStatusCode()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN);
logSuccess("미인증 회원가입 차단 확인");

logCompletion("ERP 보안 검증");
}

@Test
@DisplayName("잘못된 자격증명으로 로그인 시도 시 실패")
void loginWithInvalidCredentials_shouldFail() {
Expand Down Expand Up @@ -200,7 +153,7 @@ void loginWithInvalidCredentials_shouldFail() {
}

@SuppressWarnings("unchecked")
@Disabled
@Test
@DisplayName("중복 이메일로 사용자 등록 시도 시 실패")
void register_withDuplicateEmail_shouldFail() {
// 선행 조건: 관리자 로그인
Expand Down
Loading