diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index dd898258..0de417b1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,10 +1,10 @@ package com.kamco.cd.kamcoback.auth; +import com.kamco.cd.kamcoback.common.enums.StatusType; import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; -import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; @@ -26,38 +26,35 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { String username = authentication.getName(); String rawPassword = authentication.getCredentials().toString(); - // 1. 유저 조회 + // 유저 조회 MemberEntity member = membersRepository .findByUserId(username) .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); - // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 + // jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { // 실패 카운트 저장 int cnt = member.getLoginFailCount() + 1; if (cnt >= 5) { - member.setStatus("INACTIVE"); + member.setStatus(StatusType.INACTIVE.getId()); } member.setLoginFailCount(cnt); membersRepository.save(member); throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } - // 3. 패스워드 실패 횟수 체크 + // 삭제 상태 + if (member.getStatus().equals(StatusType.DELETED.getId())) { + throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND); + } + + // 패스워드 실패 횟수 체크 if (member.getLoginFailCount() >= 5) { throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED); } - // 4. 인증 성공 로그인 시간 저장 - if (member.getFirstLoginDttm() == null) { - member.setFirstLoginDttm(ZonedDateTime.now()); - } - member.setLastLoginDttm(ZonedDateTime.now()); - member.setLoginFailCount(0); - membersRepository.save(member); - - // 5. 인증 성공 → UserDetails 생성 + // 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java index 641d3de5..0da73338 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java @@ -9,7 +9,7 @@ import lombok.Getter; public enum StatusType implements EnumType { ACTIVE("활성"), INACTIVE("비활성"), - ARCHIVED("탈퇴"); + DELETED("삭제"); private final String desc; diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java index a96da1a3..b404cbf8 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java @@ -1,10 +1,9 @@ package com.kamco.cd.kamcoback.common.utils; import com.kamco.cd.kamcoback.auth.CustomUserDetails; -import com.kamco.cd.kamcoback.auth.JwtTokenProvider; +import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import java.util.Optional; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -13,42 +12,30 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class UserUtil { - private final JwtTokenProvider jwtTokenProvider; - - /** - * 현재 SecurityContext의 Authentication 에서 사용자 ID(Long) 가져오기 - 로그인 된 상태(AccessToken 인증 이후)에서만 사용 가능 - * - 인증 정보 없으면 null 리턴 - */ - public Long getCurrentUserId() { + public MembersDto.Member getCurrentUser() { return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getId()) + .map( + auth -> { + CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); + MemberEntity m = user.getMember(); + return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo()); + }) .orElse(null); } - /** 현재 SecurityContext의 Authentication 에서 사용자 UUID 가져오기 - 인증 정보 없으면 null 리턴 */ - public UUID getCurrentUserUuid() { - return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getUuid()) - .orElse(null); + public Long getId() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getId() : null; } - /** 현재 로그인한 사용자의 MemberEntity 통째로 가져오기 (Optional) */ - public Optional getCurrentMember() { - return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember()); + public String getName() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getName() : null; } - /** 현재 로그인한 사용자의 MemberEntity 가져오기 (없으면 예외) */ - public MemberEntity getCurrentMemberOrThrow() { - return getCurrentMember().orElseThrow(() -> new IllegalStateException("인증된 사용자를 찾을 수 없습니다.")); - // 필요하면 여기서 CustomApiException 으로 바꿔도 됨 - } - - /** AccessToken / RefreshToken 에서 sub(subject, uuid) 추출 - 토큰 문자열만 있을 때 uuid 가져올 때 사용 */ - public String getSubjectFromToken(String token) { - return jwtTokenProvider.getSubject(token); + public String getEmployeeNo() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getEmployeeNo() : null; } } 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 a0d7ed01..d80dbfdf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -1,9 +1,12 @@ package com.kamco.cd.kamcoback.members; +import com.kamco.cd.kamcoback.auth.CustomUserDetails; import com.kamco.cd.kamcoback.auth.JwtTokenProvider; import com.kamco.cd.kamcoback.auth.RefreshTokenService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.SignInRequest; +import com.kamco.cd.kamcoback.members.dto.TokenResponse; import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -15,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import java.nio.file.AccessDeniedException; import java.time.Duration; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -65,31 +69,31 @@ public class AuthController { description = "존재하지 않는 아이디", value = """ - { - "code": "LOGIN_ID_NOT_FOUND", - "message": "아이디를 잘못 입력하셨습니다." - } - """), + { + "code": "LOGIN_ID_NOT_FOUND", + "message": "아이디를 잘못 입력하셨습니다." + } + """), @ExampleObject( name = "비밀번호 입력 오류 (4회 이하)", description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", value = """ - { - "code": "LOGIN_PASSWORD_MISMATCH", - "message": "비밀번호를 잘못 입력하셨습니다." - } - """), + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다." + } + """), @ExampleObject( name = "비밀번호 오류 횟수 초과", description = "비밀번호 5회 이상 오류로 계정 잠김", value = """ - { - "code": "LOGIN_PASSWORD_EXCEEDED", - "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." - } - """) + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) })) }) public ApiResponseDto signin( @@ -106,9 +110,11 @@ public class AuthController { String status = authService.getUserStatus(request); - // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴 + MembersDto.Member member = new MembersDto.Member(); + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), DELETED 탈퇴 if (!"ACTIVE".equals(status)) { - return ApiResponseDto.ok(new TokenResponse(status, null, null)); + return ApiResponseDto.ok(new TokenResponse(status, null, null, member)); } String username = authentication.getName(); // UserDetailsService 에서 사용한 username @@ -132,7 +138,15 @@ public class AuthController { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); + CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal(); + member.setId(user.getMember().getId()); + member.setName(user.getMember().getName()); + member.setEmployeeNo(user.getMember().getEmployeeNo()); + + // 인증 성공 로그인 시간 저장 + authService.saveLogin(UUID.fromString(username)); + + return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member)); } @PostMapping("/refresh") @@ -179,7 +193,8 @@ public class AuthController { .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); + MembersDto.Member member = new MembersDto.Member(); + return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member)); } @PostMapping("/logout") @@ -211,6 +226,4 @@ public class AuthController { return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - - public record TokenResponse(String status, String accessToken, String refreshToken) {} } 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 fd11e504..7f6fe46c 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 @@ -174,4 +174,15 @@ public class MembersDto { @NotBlank private String tempPassword; } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class Member { + + private Long id; + private String name; + private String employeeNo; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java new file mode 100644 index 00000000..d410279e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java @@ -0,0 +1,16 @@ +package com.kamco.cd.kamcoback.members.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class TokenResponse { + + private String status; + private String accessToken; + private String refreshToken; + private MembersDto.Member member; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 1dcfc578..287bc99b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.members.service; import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +14,16 @@ public class AuthService { private final MembersCoreService membersCoreService; + /** + * 로그인 일시 저장 + * + * @param uuid + */ + @Transactional + public void saveLogin(UUID uuid) { + membersCoreService.saveLogin(uuid); + } + /** * 사용자 상태 조회 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 5cbd1b79..c9611d6d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -44,7 +44,7 @@ public class MembersService { } /** - * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 + * 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호 * * @param password * @return 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 6f1854d7..77039d5a 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 @@ -1,6 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; +import com.kamco.cd.kamcoback.common.enums.StatusType; +import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -23,6 +25,7 @@ import org.springframework.stereotype.Service; public class MembersCoreService { private final MembersRepository membersRepository; + private final UserUtil userUtil; /** * 관리자 계정 등록 @@ -47,6 +50,7 @@ public class MembersCoreService { memberEntity.setPassword(hashedPassword); memberEntity.setName(addReq.getName()); memberEntity.setEmployeeNo(addReq.getEmployeeNo()); + memberEntity.setRgstrUidl(userUtil.getId()); return membersRepository.save(memberEntity).getId(); } @@ -74,11 +78,13 @@ public class MembersCoreService { memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); } + memberEntity.setUpdtrUid(userUtil.getId()); + membersRepository.save(memberEntity); } /** - * 관리자 계정 미사용 처리 + * 관리자 계정 삭제 처리 * * @param uuid */ @@ -86,8 +92,9 @@ public class MembersCoreService { MemberEntity memberEntity = membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - memberEntity.setStatus("INACTIVE"); + memberEntity.setStatus(StatusType.DELETED.getId()); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(userUtil.getId()); membersRepository.save(memberEntity); } @@ -108,6 +115,7 @@ public class MembersCoreService { memberEntity.setPassword(hashedPassword); memberEntity.setStatus("ACTIVE"); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(memberEntity.getId()); membersRepository.save(memberEntity); } @@ -136,4 +144,21 @@ public class MembersCoreService { .orElseThrow(MemberNotFoundException::new); return memberEntity.getStatus(); } + + /** + * 최초 로그인 저장 마지막 로그인 저장 + * + * @param uuid + */ + public void saveLogin(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + if (memberEntity.getFirstLoginDttm() == null) { + memberEntity.setFirstLoginDttm(ZonedDateTime.now()); + } + memberEntity.setLastLoginDttm(ZonedDateTime.now()); + memberEntity.setLoginFailCount(0); + membersRepository.save(memberEntity); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java index 5a4879b0..19f7e4d3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.postgres.entity; +import com.kamco.cd.kamcoback.common.enums.StatusType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -63,7 +64,7 @@ public class MemberEntity { @Size(max = 20) @ColumnDefault("'INACTIVE'") @Column(name = "status", length = 20) - private String status = "INACTIVE"; + private String status = StatusType.INACTIVE.getId(); @NotNull @ColumnDefault("now()") @@ -83,4 +84,10 @@ public class MemberEntity { @Column(name = "login_fail_count") @ColumnDefault("0") private Integer loginFailCount = 0; + + @Column(name = "rgstr_uid") + private Long rgstrUidl; + + @Column(name = "updtr_uid") + private Long updtrUid; }