diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index 5d7b6be1..d4005a3f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -26,39 +26,41 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - // .sessionManagement( - // sm -> - // sm.sessionCreationPolicy( - // SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음 요청은 JWT 인증 - // .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - // .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - // .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - // .authenticationProvider( - // customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 - // .authorizeHttpRequests( - // auth -> - // auth.requestMatchers( - // "/api/auth/signin", - // "/api/auth/refresh", - // "/swagger-ui/**", - // "/v3/api-docs/**") - // .permitAll() - // .anyRequest() - // .authenticated()) - // .addFilterBefore( - // jwtAuthenticationFilter, - // UsernamePasswordAuthenticationFilter - // .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + /* + http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 + .sessionManagement( + sm -> + sm.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 + .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 + .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 + .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 + .authenticationProvider( + customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .authorizeHttpRequests( + auth -> + auth.requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. +*/ http.csrf(csrf -> csrf.disable()) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .formLogin(form -> form.disable()) - .httpBasic(basic -> basic.disable()) - .logout(logout -> logout.disable()) - .authenticationProvider(customAuthenticationProvider) - .authorizeHttpRequests( - auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음 - ); + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()) + .logout(logout -> logout.disable()) + .authenticationProvider(customAuthenticationProvider) + .authorizeHttpRequests( + auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음 + ); ; return http.build(); @@ -66,10 +68,15 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) - throws Exception { + throws Exception { return configuration.getAuthenticationManager(); } + /** + * CORS 설정 + * + * @return + */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성 diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SwaggerConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SwaggerConfig.java index bfd430f1..1290b4c7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SwaggerConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SwaggerConfig.java @@ -1,10 +1,12 @@ package com.kamco.cd.kamcoback.config; -// @Configuration -// @SecurityScheme( +//@Configuration +//@SecurityScheme( // name = "BearerAuth", // type = SecuritySchemeType.HTTP, // scheme = "bearer", // bearerFormat = "JWT" -// ) -public class SwaggerConfig {} +//) +public class SwaggerConfig { + +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java index 93c3bddf..67536bdb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiLogFunction.java @@ -35,7 +35,6 @@ public class ApiLogFunction { // 사용자 ID 추출 예시 (Spring Security 기준) public static String getUserId(HttpServletRequest request) { try { - Object userId = request.getUserPrincipal(); return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null; } catch (Exception e) { return null; @@ -65,22 +64,22 @@ public class ApiLogFunction { } public static String getRequestBody( - HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) { + HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) { StringBuilder resultBody = new StringBuilder(); // GET, form-urlencoded POST 파라미터 Map paramMap = servletRequest.getParameterMap(); String queryParams = - paramMap.entrySet().stream() - .map(e -> e.getKey() + "=" + String.join(",", e.getValue())) - .collect(Collectors.joining("&")); + paramMap.entrySet().stream() + .map(e -> e.getKey() + "=" + String.join(",", e.getValue())) + .collect(Collectors.joining("&")); resultBody.append(queryParams.isEmpty() ? "" : queryParams); // JSON Body if ("POST".equalsIgnoreCase(servletRequest.getMethod()) - && servletRequest.getContentType() != null - && servletRequest.getContentType().contains("application/json")) { + && servletRequest.getContentType() != null + && servletRequest.getContentType().contains("application/json")) { try { // json인 경우는 Wrapper를 통해 가져오기 resultBody.append(getBodyData(contentWrapper)); @@ -92,8 +91,8 @@ public class ApiLogFunction { // Multipart form-data if ("POST".equalsIgnoreCase(servletRequest.getMethod()) - && servletRequest.getContentType() != null - && servletRequest.getContentType().startsWith("multipart/form-data")) { + && servletRequest.getContentType() != null + && servletRequest.getContentType().startsWith("multipart/form-data")) { resultBody.append("multipart/form-data request"); } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java index 61fd661e..9d2f2caf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java @@ -1,15 +1,16 @@ package com.kamco.cd.kamcoback.config.api; +import com.kamco.cd.kamcoback.auth.CustomUserDetails; import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity; import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; import jakarta.servlet.http.HttpServletRequest; -import java.util.Optional; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.util.ContentCachingRequestWrapper; @@ -30,19 +31,19 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports( - MethodParameter returnType, Class> converterType) { + MethodParameter returnType, Class> converterType) { // ApiResponseDto를 반환하는 경우에만 적용 return returnType.getParameterType().equals(ApiResponseDto.class); } @Override public Object beforeBodyWrite( - Object body, - MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, - ServerHttpResponse response) { + Object body, + MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest; @@ -52,21 +53,34 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { response.setStatusCode(apiResponse.getHttpStatus()); String ip = ApiLogFunction.getClientIp(servletRequest); - // TODO : userid 가 계정명인지, uid 인지 확인 후 로직 수정 필요함 - Long userid = - Long.valueOf(Optional.ofNullable(ApiLogFunction.getUserId(servletRequest)).orElse("1")); + Long userid = null; + + /** + * servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth + * 이 요청이 JWT 인증을 통과한 요청인가? 그리고 Spring Security Authentication 객체가 UsernamePasswordAuthenticationToken 타입인가? 체크 + */ + /** + * auth.getPrincipal() instanceof CustomUserDetails customUserDetails + * principal 안에 들어있는 객체가 내가 만든 CustomUserDetails 타입인가? 체크 + */ + if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth + && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) { + + // audit 에는 long 타입 user_id가 들어가지만 토큰 sub은 uuid여서 user_id 가져오기 + userid = customUserDetails.getMember().getId(); + } // TODO: menuUid 를 동적으로 가져오게끔 해야함 AuditLogEntity log = - new AuditLogEntity( - userid, - ApiLogFunction.getEventType(servletRequest), - ApiLogFunction.isSuccessFail(apiResponse), - "MU_01_01", - ip, - servletRequest.getRequestURI(), - ApiLogFunction.getRequestBody(servletRequest, contentWrapper), - apiResponse.getErrorLogUid()); + new AuditLogEntity( + userid, + ApiLogFunction.getEventType(servletRequest), + ApiLogFunction.isSuccessFail(apiResponse), + "MU_01_01", + ip, + servletRequest.getRequestURI(), + ApiLogFunction.getRequestBody(servletRequest, contentWrapper), + apiResponse.getErrorLogUid()); // tb_audit_log 테이블 저장 auditLogRepository.save(log); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index e52e2f97..dcd1d53b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -28,159 +30,160 @@ public class AdminApiController { @Operation(summary = "회원가입", description = "회원가입") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "회원가입 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "회원가입 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/join") public ApiResponseDto saveMember( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "회원가입", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.AddReq.class))) - @RequestBody - @Valid - MembersDto.AddReq addReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "회원가입", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.AddReq.class))) + @RequestBody + @Valid + MembersDto.AddReq addReq) { return ApiResponseDto.createOK(adminService.saveMember(addReq)); } @Operation(summary = "역할 추가", description = "uuid 기준으로 역할 추가") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "역할 추가", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "역할 추가", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/roles/add") public ApiResponseDto saveRoles( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "역할 추가", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.RolesDto.class))) - @RequestBody - @Valid - MembersDto.RolesDto rolesDto) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "역할 추가", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.RolesDto.class))) + @RequestBody + @Valid + MembersDto.RolesDto rolesDto) { adminService.saveRoles(rolesDto); return ApiResponseDto.createOK(rolesDto.getUuid()); } @Operation(summary = "역할 삭제", description = "uuid 기준으로 역할 삭제") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "역할 삭제", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PostMapping("/roles/rm") + value = { + @ApiResponse( + responseCode = "201", + description = "역할 삭제", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @DeleteMapping("/roles/rm") public ApiResponseDto deleteRoles( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "역할 삭제", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.RolesDto.class))) - @RequestBody - @Valid - MembersDto.RolesDto rolesDto) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "역할 삭제", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.RolesDto.class))) + @RequestBody + @Valid + MembersDto.RolesDto rolesDto) { adminService.deleteRoles(rolesDto); return ApiResponseDto.createOK(rolesDto.getUuid()); } @Operation(summary = "상태 수정", description = "상태 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "상태 수정", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PostMapping("/status/update") + value = { + @ApiResponse( + responseCode = "201", + description = "상태 수정", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PatchMapping("{uuid}/status") public ApiResponseDto updateStatus( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "상태 수정", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.StatusDto.class))) - @RequestBody - @Valid - MembersDto.StatusDto statusDto) { - adminService.updateStatus(statusDto); - return ApiResponseDto.createOK(statusDto.getUuid()); + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "상태 수정", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.StatusDto.class))) + @PathVariable UUID uuid, + @RequestBody + @Valid + MembersDto.StatusDto statusDto) { + adminService.updateStatus(uuid, statusDto); + return ApiResponseDto.createOK(uuid); } @Operation(summary = "회원 탈퇴", description = "회원 탈퇴") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "회원 탈퇴", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PostMapping("/delete-account") - public ApiResponseDto deleteAccount(MembersDto.StatusDto statusDto) { - adminService.deleteAccount(statusDto); - return ApiResponseDto.createOK(statusDto.getUuid()); + value = { + @ApiResponse( + responseCode = "201", + description = "회원 탈퇴", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @DeleteMapping("/delete/{uuid}") + public ApiResponseDto deleteAccount(@PathVariable UUID uuid) { + adminService.deleteAccount(uuid); + return ApiResponseDto.createOK(uuid); } - @Operation(summary = "패스워드 초기화", description = "패스워드 초기화") + @Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "패스워드 초기화", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PostMapping("/{memberId}/password") + value = { + @ApiResponse( + responseCode = "201", + description = "비밀번호 초기화", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PatchMapping("/{memberId}/password") public ApiResponseDto resetPassword(@PathVariable Long memberId) { adminService.resetPassword(memberId); return ApiResponseDto.createOK(memberId); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 55f99170..f7a49aa7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -41,27 +41,27 @@ public class AuthController { private boolean refreshCookieSecure; @PostMapping("/signin") - @Operation(summary = "로그인", description = "사번 또는 이메일과 비밀번호로 로그인하여 액세스/리프레시 토큰을 발급합니다.") + @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "ID 또는 비밀번호 불일치", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "ID 또는 비밀번호 불일치", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.username(), request.password())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.username(), request.password())); String username = authentication.getName(); // UserDetailsService 에서 사용한 username @@ -70,17 +70,17 @@ public class AuthController { // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) // 로컬 개발에서 http만 쓰면 false 로 바꿔야 할 수도 있음 - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); @@ -91,13 +91,13 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { @@ -117,29 +117,29 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ResponseEntity.ok(new TokenResponse(newAccessToken)); } @PostMapping("/logout") - @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(블랙리스트 처리 또는 리프레시 토큰 삭제)합니다.") + @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ResponseEntity logout(Authentication authentication, HttpServletResponse response) { if (authentication != null) { @@ -150,13 +150,13 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ResponseEntity.noContent().build(); @@ -164,8 +164,12 @@ public class AuthController { @Schema(description = "로그인 요청 DTO") public record SignInRequest( - @Schema(description = "사번", example = "11111") String username, - @Schema(description = "비밀번호", example = "kamco1234!") String password) {} + @Schema(description = "사번", example = "11111") String username, + @Schema(description = "비밀번호", example = "kamco1234!") String password) { - public record TokenResponse(String accessToken) {} + } + + public record TokenResponse(String accessToken) { + + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 78eeb10f..5993e8cd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -16,7 +16,7 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,40 +31,40 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } @Operation(summary = "회원정보 수정", description = "회원정보 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "회원정보 수정", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PostMapping("/{uuid}") + value = { + @ApiResponse( + responseCode = "201", + description = "회원정보 수정", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PutMapping("/{uuid}") public ApiResponseDto updateMember( - @PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { + @PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { membersService.updateMember(uuid, updateReq); return ApiResponseDto.createOK(uuid); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index 2137cb59..cf1b9bca 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -28,19 +28,21 @@ public class MembersDto { private String email; private String status; private String roleName; - @JsonFormatDttm private ZonedDateTime createdDttm; - @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm + private ZonedDateTime createdDttm; + @JsonFormatDttm + private ZonedDateTime updatedDttm; public Basic( - Long id, - UUID uuid, - String employeeNo, - String name, - String email, - String status, - String roleName, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm) { + Long id, + UUID uuid, + String employeeNo, + String name, + String email, + String status, + String roleName, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm) { this.id = id; this.uuid = uuid; this.employeeNo = employeeNo; @@ -166,10 +168,6 @@ public class MembersDto { @AllArgsConstructor public static class StatusDto { - @Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5") - @NotBlank - private UUID uuid; - @Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE") @NotBlank private String status; diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java index 46b5a0da..7174d9b5 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java @@ -3,6 +3,7 @@ package com.kamco.cd.kamcoback.members.service; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.beans.factory.annotation.Value; @@ -60,17 +61,17 @@ public class AdminService { * * @param statusDto */ - public void updateStatus(MembersDto.StatusDto statusDto) { - membersCoreService.updateStatus(statusDto); + public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { + membersCoreService.updateStatus(uuid, statusDto); } /** * 회원 탈퇴 * - * @param statusDto + * @param uuid */ - public void deleteAccount(MembersDto.StatusDto statusDto) { - membersCoreService.deleteAccount(statusDto); + public void deleteAccount(UUID uuid) { + membersCoreService.deleteAccount(uuid); } /** diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index 9287f3fb..e79a241a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -41,12 +41,12 @@ public class MembersCoreService { public Long saveMembers(MembersDto.AddReq addReq) { if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) { throw new MemberException.DuplicateMemberException( - MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo()); + MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo()); } if (membersRepository.existsByEmail(addReq.getEmail())) { throw new MemberException.DuplicateMemberException( - MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail()); + MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail()); } MemberEntity memberEntity = new MemberEntity(); @@ -66,7 +66,7 @@ public class MembersCoreService { */ public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) { memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); @@ -93,13 +93,13 @@ public class MembersCoreService { public void saveRoles(MembersDto.RolesDto rolesDto) { MemberEntity memberEntity = - membersRepository - .findByUUID(rolesDto.getUuid()) - .orElseThrow(() -> new MemberNotFoundException()); + membersRepository + .findByUUID(rolesDto.getUuid()) + .orElseThrow(() -> new MemberNotFoundException()); if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) { throw new MemberException.DuplicateMemberException( - MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); + MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); } MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); @@ -120,9 +120,9 @@ public class MembersCoreService { */ public void deleteRoles(MembersDto.RolesDto rolesDto) { MemberEntity memberEntity = - membersRepository - .findByUUID(rolesDto.getUuid()) - .orElseThrow(() -> new MemberNotFoundException()); + membersRepository + .findByUUID(rolesDto.getUuid()) + .orElseThrow(() -> new MemberNotFoundException()); MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); @@ -140,11 +140,11 @@ public class MembersCoreService { * * @param statusDto */ - public void updateStatus(MembersDto.StatusDto statusDto) { + public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { MemberEntity memberEntity = - membersRepository - .findByUUID(statusDto.getUuid()) - .orElseThrow(() -> new MemberNotFoundException()); + membersRepository + .findByUUID(uuid) + .orElseThrow(() -> new MemberNotFoundException()); memberEntity.setStatus(statusDto.getStatus()); memberEntity.setUpdatedDttm(ZonedDateTime.now()); @@ -154,13 +154,13 @@ public class MembersCoreService { /** * 회원 탈퇴 * - * @param statusDto + * @param uuid */ - public void deleteAccount(MembersDto.StatusDto statusDto) { + public void deleteAccount(UUID uuid) { MemberEntity memberEntity = - membersRepository - .findByUUID(statusDto.getUuid()) - .orElseThrow(() -> new MemberNotFoundException()); + membersRepository + .findByUUID(uuid) + .orElseThrow(() -> new MemberNotFoundException()); MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId(); memberArchivedEntityId.setUserId(memberEntity.getId()); @@ -193,10 +193,10 @@ public class MembersCoreService { */ public void resetPassword(Long id) { MemberEntity memberEntity = - membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); String salt = - BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화, 초기 패스워드 고정 String hashedPassword = BCrypt.hashpw(password, salt);