로그인 기능 추가

This commit is contained in:
2025-12-04 16:51:24 +09:00
parent a7d03a0086
commit f41e82e3ca
10 changed files with 335 additions and 307 deletions

View File

@@ -26,39 +26,41 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 /*
// .sessionManagement( http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화
// sm -> .sessionManagement(
// sm.sessionCreationPolicy( sm ->
// SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음 요청은 JWT 인증 sm.sessionCreationPolicy(
// .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증
// .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 .formLogin(form -> form.disable()) // react에서 로그인 요청 관리
// .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용
// .authenticationProvider( .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음
// customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 .authenticationProvider(
// .authorizeHttpRequests( customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용
// auth -> .authorizeHttpRequests(
// auth.requestMatchers( auth ->
// "/api/auth/signin", auth.requestMatchers(
// "/api/auth/refresh", "/api/auth/signin",
// "/swagger-ui/**", "/api/auth/refresh",
// "/v3/api-docs/**") "/swagger-ui/**",
// .permitAll() "/v3/api-docs/**")
// .anyRequest() .permitAll()
// .authenticated()) .anyRequest()
// .addFilterBefore( .authenticated())
// jwtAuthenticationFilter, .addFilterBefore(
// UsernamePasswordAuthenticationFilter jwtAuthenticationFilter,
// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. UsernamePasswordAuthenticationFilter
.class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장.
*/
http.csrf(csrf -> csrf.disable()) http.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable()) .formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable()) .httpBasic(basic -> basic.disable())
.logout(logout -> logout.disable()) .logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider) .authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests( .authorizeHttpRequests(
auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음 auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음
); );
; ;
return http.build(); return http.build();
@@ -66,10 +68,15 @@ public class SecurityConfig {
@Bean @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception { throws Exception {
return configuration.getAuthenticationManager(); return configuration.getAuthenticationManager();
} }
/**
* CORS 설정
*
* @return
*/
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성 CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성

View File

@@ -1,10 +1,12 @@
package com.kamco.cd.kamcoback.config; package com.kamco.cd.kamcoback.config;
// @Configuration //@Configuration
// @SecurityScheme( //@SecurityScheme(
// name = "BearerAuth", // name = "BearerAuth",
// type = SecuritySchemeType.HTTP, // type = SecuritySchemeType.HTTP,
// scheme = "bearer", // scheme = "bearer",
// bearerFormat = "JWT" // bearerFormat = "JWT"
// ) //)
public class SwaggerConfig {} public class SwaggerConfig {
}

View File

@@ -35,7 +35,6 @@ public class ApiLogFunction {
// 사용자 ID 추출 예시 (Spring Security 기준) // 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) { public static String getUserId(HttpServletRequest request) {
try { try {
Object userId = request.getUserPrincipal();
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null; return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
@@ -65,22 +64,22 @@ public class ApiLogFunction {
} }
public static String getRequestBody( public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) { HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder(); StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터 // GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap(); Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams = String queryParams =
paramMap.entrySet().stream() paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue())) .map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&")); .collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams); resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body // JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod()) if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null && servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) { && servletRequest.getContentType().contains("application/json")) {
try { try {
// json인 경우는 Wrapper를 통해 가져오기 // json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper)); resultBody.append(getBodyData(contentWrapper));
@@ -92,8 +91,8 @@ public class ApiLogFunction {
// Multipart form-data // Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod()) if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null && servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) { && servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request"); resultBody.append("multipart/form-data request");
} }

View File

@@ -1,15 +1,16 @@
package com.kamco.cd.kamcoback.config.api; 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.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository; import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingRequestWrapper;
@@ -30,19 +31,19 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
@Override @Override
public boolean supports( public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용 // ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class); return returnType.getParameterType().equals(ApiResponseDto.class);
} }
@Override @Override
public Object beforeBodyWrite( public Object beforeBodyWrite(
Object body, Object body,
MethodParameter returnType, MethodParameter returnType,
MediaType selectedContentType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpRequest request,
ServerHttpResponse response) { ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest; ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest;
@@ -52,21 +53,34 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
response.setStatusCode(apiResponse.getHttpStatus()); response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest); 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 를 동적으로 가져오게끔 해야함 // TODO: menuUid 를 동적으로 가져오게끔 해야함
AuditLogEntity log = AuditLogEntity log =
new AuditLogEntity( new AuditLogEntity(
userid, userid,
ApiLogFunction.getEventType(servletRequest), ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse), ApiLogFunction.isSuccessFail(apiResponse),
"MU_01_01", "MU_01_01",
ip, ip,
servletRequest.getRequestURI(), servletRequest.getRequestURI(),
ApiLogFunction.getRequestBody(servletRequest, contentWrapper), ApiLogFunction.getRequestBody(servletRequest, contentWrapper),
apiResponse.getErrorLogUid()); apiResponse.getErrorLogUid());
// tb_audit_log 테이블 저장 // tb_audit_log 테이블 저장
auditLogRepository.save(log); auditLogRepository.save(log);

View File

@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; 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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -28,159 +30,160 @@ public class AdminApiController {
@Operation(summary = "회원가입", description = "회원가입") @Operation(summary = "회원가입", description = "회원가입")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "회원가입 성공", description = "회원가입 성공",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = Long.class))), schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/join") @PostMapping("/join")
public ApiResponseDto<Long> saveMember( public ApiResponseDto<Long> saveMember(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원가입", description = "회원가입",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.AddReq.class))) schema = @Schema(implementation = MembersDto.AddReq.class)))
@RequestBody @RequestBody
@Valid @Valid
MembersDto.AddReq addReq) { MembersDto.AddReq addReq) {
return ApiResponseDto.createOK(adminService.saveMember(addReq)); return ApiResponseDto.createOK(adminService.saveMember(addReq));
} }
@Operation(summary = "역할 추가", description = "uuid 기준으로 역할 추가") @Operation(summary = "역할 추가", description = "uuid 기준으로 역할 추가")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "역할 추가", description = "역할 추가",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/roles/add") @PostMapping("/roles/add")
public ApiResponseDto<UUID> saveRoles( public ApiResponseDto<UUID> saveRoles(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 추가", description = "역할 추가",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class))) schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody @RequestBody
@Valid @Valid
MembersDto.RolesDto rolesDto) { MembersDto.RolesDto rolesDto) {
adminService.saveRoles(rolesDto); adminService.saveRoles(rolesDto);
return ApiResponseDto.createOK(rolesDto.getUuid()); return ApiResponseDto.createOK(rolesDto.getUuid());
} }
@Operation(summary = "역할 삭제", description = "uuid 기준으로 역할 삭제") @Operation(summary = "역할 삭제", description = "uuid 기준으로 역할 삭제")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "역할 삭제", description = "역할 삭제",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/roles/rm") @DeleteMapping("/roles/rm")
public ApiResponseDto<UUID> deleteRoles( public ApiResponseDto<UUID> deleteRoles(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 삭제", description = "역할 삭제",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class))) schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody @RequestBody
@Valid @Valid
MembersDto.RolesDto rolesDto) { MembersDto.RolesDto rolesDto) {
adminService.deleteRoles(rolesDto); adminService.deleteRoles(rolesDto);
return ApiResponseDto.createOK(rolesDto.getUuid()); return ApiResponseDto.createOK(rolesDto.getUuid());
} }
@Operation(summary = "상태 수정", description = "상태 수정") @Operation(summary = "상태 수정", description = "상태 수정")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "상태 수정", description = "상태 수정",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/status/update") @PatchMapping("{uuid}/status")
public ApiResponseDto<UUID> updateStatus( public ApiResponseDto<UUID> updateStatus(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "상태 수정", description = "상태 수정",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.StatusDto.class))) schema = @Schema(implementation = MembersDto.StatusDto.class)))
@RequestBody @PathVariable UUID uuid,
@Valid @RequestBody
MembersDto.StatusDto statusDto) { @Valid
adminService.updateStatus(statusDto); MembersDto.StatusDto statusDto) {
return ApiResponseDto.createOK(statusDto.getUuid()); adminService.updateStatus(uuid, statusDto);
return ApiResponseDto.createOK(uuid);
} }
@Operation(summary = "회원 탈퇴", description = "회원 탈퇴") @Operation(summary = "회원 탈퇴", description = "회원 탈퇴")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "회원 탈퇴", description = "회원 탈퇴",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/delete-account") @DeleteMapping("/delete/{uuid}")
public ApiResponseDto<UUID> deleteAccount(MembersDto.StatusDto statusDto) { public ApiResponseDto<UUID> deleteAccount(@PathVariable UUID uuid) {
adminService.deleteAccount(statusDto); adminService.deleteAccount(uuid);
return ApiResponseDto.createOK(statusDto.getUuid()); return ApiResponseDto.createOK(uuid);
} }
@Operation(summary = "패스워드 초기화", description = "패스워드 초기화") @Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "패스워드 초기화", description = "비밀번호 초기화",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = Long.class))), schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/{memberId}/password") @PatchMapping("/{memberId}/password")
public ApiResponseDto<Long> resetPassword(@PathVariable Long memberId) { public ApiResponseDto<Long> resetPassword(@PathVariable Long memberId) {
adminService.resetPassword(memberId); adminService.resetPassword(memberId);
return ApiResponseDto.createOK(memberId); return ApiResponseDto.createOK(memberId);

View File

@@ -41,27 +41,27 @@ public class AuthController {
private boolean refreshCookieSecure; private boolean refreshCookieSecure;
@PostMapping("/signin") @PostMapping("/signin")
@Operation(summary = "로그인", description = "사번 또는 이메일과 비밀번호로 로그인하여 액세스/리프레시 토큰을 발급합니다.") @Operation(summary = "로그인", description = "사번로 로그인하여 액세스/리프레시 토큰을 발급.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "로그인 성공", description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))), content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse( @ApiResponse(
responseCode = "401", responseCode = "401",
description = "ID 또는 비밀번호 불일치", description = "ID 또는 비밀번호 불일치",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
}) })
public ResponseEntity<TokenResponse> signin( public ResponseEntity<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보", description = "로그인 요청 정보",
required = true) required = true)
@RequestBody @RequestBody
SignInRequest request, SignInRequest request,
HttpServletResponse response) { HttpServletResponse response) {
Authentication authentication = Authentication authentication =
authenticationManager.authenticate( authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())); new UsernamePasswordAuthenticationToken(request.username(), request.password()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username String username = authentication.getName(); // UserDetailsService 에서 사용한 username
@@ -70,17 +70,17 @@ public class AuthController {
// Redis에 RefreshToken 저장 (TTL = 7일) // Redis에 RefreshToken 저장 (TTL = 7일)
refreshTokenService.save( refreshTokenService.save(
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장 // HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken) ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) // 로컬 개발에서 http만 쓰면 false 로 바꿔야 할 수도 있음 .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
@@ -91,13 +91,13 @@ public class AuthController {
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "재발급 성공", description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))), content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse( @ApiResponse(
responseCode = "401", responseCode = "401",
description = "만료되었거나 유효하지 않은 리프레시 토큰", description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
}) })
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) { public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
@@ -117,29 +117,29 @@ public class AuthController {
// Redis 갱신 // Redis 갱신
refreshTokenService.save( refreshTokenService.save(
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신 // 쿠키 갱신
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, newRefreshToken) ResponseCookie.from(refreshCookieName, newRefreshToken)
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(new TokenResponse(newAccessToken)); return ResponseEntity.ok(new TokenResponse(newAccessToken));
} }
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(블랙리스트 처리 또는 리프레시 토큰 삭제)합니다.") @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "로그아웃 성공", description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class))) content = @Content(schema = @Schema(implementation = Void.class)))
}) })
public ResponseEntity<Void> logout(Authentication authentication, HttpServletResponse response) { public ResponseEntity<Void> logout(Authentication authentication, HttpServletResponse response) {
if (authentication != null) { if (authentication != null) {
@@ -150,13 +150,13 @@ public class AuthController {
// 쿠키 삭제 (Max-Age=0) // 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "") ResponseCookie.from(refreshCookieName, "")
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(0) .maxAge(0)
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -164,8 +164,12 @@ public class AuthController {
@Schema(description = "로그인 요청 DTO") @Schema(description = "로그인 요청 DTO")
public record SignInRequest( public record SignInRequest(
@Schema(description = "사번", example = "11111") String username, @Schema(description = "사번", example = "11111") String username,
@Schema(description = "비밀번호", example = "kamco1234!") String password) {} @Schema(description = "비밀번호", example = "kamco1234!") String password) {
public record TokenResponse(String accessToken) {} }
public record TokenResponse(String accessToken) {
}
} }

View File

@@ -16,7 +16,7 @@ import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -31,40 +31,40 @@ public class MembersApiController {
@Operation(summary = "회원정보 목록", description = "회원정보 조회") @Operation(summary = "회원정보 목록", description = "회원정보 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "검색 성공", description = "검색 성공",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = Page.class))), schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@GetMapping @GetMapping
public ApiResponseDto<Page<Basic>> getMemberList( public ApiResponseDto<Page<Basic>> getMemberList(
@ParameterObject MembersDto.SearchReq searchReq) { @ParameterObject MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq)); return ApiResponseDto.ok(membersService.findByMembers(searchReq));
} }
@Operation(summary = "회원정보 수정", description = "회원정보 수정") @Operation(summary = "회원정보 수정", description = "회원정보 수정")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "회원정보 수정", description = "회원정보 수정",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PostMapping("/{uuid}") @PutMapping("/{uuid}")
public ApiResponseDto<UUID> updateMember( public ApiResponseDto<UUID> updateMember(
@PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { @PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) {
membersService.updateMember(uuid, updateReq); membersService.updateMember(uuid, updateReq);
return ApiResponseDto.createOK(uuid); return ApiResponseDto.createOK(uuid);
} }

View File

@@ -28,19 +28,21 @@ public class MembersDto {
private String email; private String email;
private String status; private String status;
private String roleName; private String roleName;
@JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm
@JsonFormatDttm private ZonedDateTime updatedDttm; private ZonedDateTime createdDttm;
@JsonFormatDttm
private ZonedDateTime updatedDttm;
public Basic( public Basic(
Long id, Long id,
UUID uuid, UUID uuid,
String employeeNo, String employeeNo,
String name, String name,
String email, String email,
String status, String status,
String roleName, String roleName,
ZonedDateTime createdDttm, ZonedDateTime createdDttm,
ZonedDateTime updatedDttm) { ZonedDateTime updatedDttm) {
this.id = id; this.id = id;
this.uuid = uuid; this.uuid = uuid;
this.employeeNo = employeeNo; this.employeeNo = employeeNo;
@@ -166,10 +168,6 @@ public class MembersDto {
@AllArgsConstructor @AllArgsConstructor
public static class StatusDto { public static class StatusDto {
@Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5")
@NotBlank
private UUID uuid;
@Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE") @Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE")
@NotBlank @NotBlank
private String status; private String status;

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -60,17 +61,17 @@ public class AdminService {
* *
* @param statusDto * @param statusDto
*/ */
public void updateStatus(MembersDto.StatusDto statusDto) { public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
membersCoreService.updateStatus(statusDto); membersCoreService.updateStatus(uuid, statusDto);
} }
/** /**
* 회원 탈퇴 * 회원 탈퇴
* *
* @param statusDto * @param uuid
*/ */
public void deleteAccount(MembersDto.StatusDto statusDto) { public void deleteAccount(UUID uuid) {
membersCoreService.deleteAccount(statusDto); membersCoreService.deleteAccount(uuid);
} }
/** /**

View File

@@ -41,12 +41,12 @@ public class MembersCoreService {
public Long saveMembers(MembersDto.AddReq addReq) { public Long saveMembers(MembersDto.AddReq addReq) {
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) { if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new MemberException.DuplicateMemberException( throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo()); MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo());
} }
if (membersRepository.existsByEmail(addReq.getEmail())) { if (membersRepository.existsByEmail(addReq.getEmail())) {
throw new MemberException.DuplicateMemberException( throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail()); MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail());
} }
MemberEntity memberEntity = new MemberEntity(); MemberEntity memberEntity = new MemberEntity();
@@ -66,7 +66,7 @@ public class MembersCoreService {
*/ */
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) { if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); memberEntity.setEmployeeNo(updateReq.getEmployeeNo());
@@ -93,13 +93,13 @@ public class MembersCoreService {
public void saveRoles(MembersDto.RolesDto rolesDto) { public void saveRoles(MembersDto.RolesDto rolesDto) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository membersRepository
.findByUUID(rolesDto.getUuid()) .findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException()); .orElseThrow(() -> new MemberNotFoundException());
if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) { if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) {
throw new MemberException.DuplicateMemberException( throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다.");
} }
MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
@@ -120,9 +120,9 @@ public class MembersCoreService {
*/ */
public void deleteRoles(MembersDto.RolesDto rolesDto) { public void deleteRoles(MembersDto.RolesDto rolesDto) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository membersRepository
.findByUUID(rolesDto.getUuid()) .findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException()); .orElseThrow(() -> new MemberNotFoundException());
MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); memberRoleEntityId.setMemberUuid(rolesDto.getUuid());
@@ -140,11 +140,11 @@ public class MembersCoreService {
* *
* @param statusDto * @param statusDto
*/ */
public void updateStatus(MembersDto.StatusDto statusDto) { public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository membersRepository
.findByUUID(statusDto.getUuid()) .findByUUID(uuid)
.orElseThrow(() -> new MemberNotFoundException()); .orElseThrow(() -> new MemberNotFoundException());
memberEntity.setStatus(statusDto.getStatus()); memberEntity.setStatus(statusDto.getStatus());
memberEntity.setUpdatedDttm(ZonedDateTime.now()); 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 = MemberEntity memberEntity =
membersRepository membersRepository
.findByUUID(statusDto.getUuid()) .findByUUID(uuid)
.orElseThrow(() -> new MemberNotFoundException()); .orElseThrow(() -> new MemberNotFoundException());
MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId(); MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId();
memberArchivedEntityId.setUserId(memberEntity.getId()); memberArchivedEntityId.setUserId(memberEntity.getId());
@@ -193,10 +193,10 @@ public class MembersCoreService {
*/ */
public void resetPassword(Long id) { public void resetPassword(Long id) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException());
String salt = String salt =
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
// 패스워드 암호화, 초기 패스워드 고정 // 패스워드 암호화, 초기 패스워드 고정
String hashedPassword = BCrypt.hashpw(password, salt); String hashedPassword = BCrypt.hashpw(password, salt);