-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 로그인 5회 실패 시 계정 5분 잠금 설정 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
172e651
68dd0da
99310ed
1a69279
c04c28e
8769fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ name: CI/CD with AWS ECR | |
| on: | ||
| push: | ||
| branches: | ||
| - dev | ||
| - main | ||
|
|
||
| # 워크플로우에서 사용할 공통 변수 설정 | ||
| env: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,13 +14,20 @@ | |||||||||
| import until.the.eternity.das.common.exception.CustomException; | ||||||||||
| import until.the.eternity.das.common.exception.GlobalExceptionCode; | ||||||||||
| import until.the.eternity.das.common.util.JwtUtil; | ||||||||||
| import until.the.eternity.das.login.entity.AccountLock; | ||||||||||
| import until.the.eternity.das.login.entity.AccountLockRepository; | ||||||||||
| import until.the.eternity.das.login.entity.LoginHistory; | ||||||||||
| import until.the.eternity.das.login.entity.LoginHistoryRepository; | ||||||||||
| import until.the.eternity.das.login.entity.enums.Reason; | ||||||||||
| import until.the.eternity.das.role.entity.Role; | ||||||||||
| import until.the.eternity.das.role.entity.RoleRepository; | ||||||||||
| import until.the.eternity.das.role.entity.enums.Name; | ||||||||||
| import until.the.eternity.das.token.application.TokenService; | ||||||||||
| import until.the.eternity.das.user.entity.User; | ||||||||||
| import until.the.eternity.das.user.entity.UserRepository; | ||||||||||
|
|
||||||||||
| import java.time.LocalDateTime; | ||||||||||
|
|
||||||||||
| @Slf4j | ||||||||||
| @Service | ||||||||||
| @RequiredArgsConstructor | ||||||||||
|
|
@@ -29,6 +36,8 @@ public class AuthService { | |||||||||
| private final AuthConverter authConverter; | ||||||||||
| private final UserRepository userRepository; | ||||||||||
| private final RoleRepository roleRepository; | ||||||||||
| private final AccountLockRepository accountLockRepository; | ||||||||||
| private final LoginHistoryRepository loginHistoryRepository; | ||||||||||
| private final BCryptPasswordEncoder bCryptPasswordEncoder; | ||||||||||
| private final JwtUtil jwtUtil; | ||||||||||
| private final TokenService tokenService; | ||||||||||
|
|
@@ -63,14 +72,35 @@ public SignUpResponse signUpAdmin(SignUpRequest request) { | |||||||||
| } | ||||||||||
|
|
||||||||||
| @Transactional | ||||||||||
| public LoginResultResponse login(LoginRequest request) { | ||||||||||
| public LoginResultResponse login(LoginRequest request, String clientIp, String userAgent) { | ||||||||||
| User user = userRepository.findByEmail(request.email()) | ||||||||||
| .orElseThrow(() -> new CustomException(GlobalExceptionCode.USER_NOT_EXISTS)); | ||||||||||
|
|
||||||||||
| AccountLock accountLock = accountLockRepository.findById(user.getId()) | ||||||||||
| .orElseGet(() -> { | ||||||||||
| AccountLock newLock = AccountLock.builder() | ||||||||||
| .user(user) | ||||||||||
| .userId(user.getId()) | ||||||||||
| .failedAttempts(0) | ||||||||||
| .updatedAt(LocalDateTime.now()) | ||||||||||
| .build(); | ||||||||||
| return accountLockRepository.save(newLock); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| if (accountLock.getLockedUntil() != null && accountLock.getLockedUntil() | ||||||||||
| .isAfter(LocalDateTime.now())) { | ||||||||||
| // 잠금 상태인 경우 이력 저장 후 예외 발생 | ||||||||||
| saveLoginHistory(user, false, Reason.LOCKED_ACCOUNT, clientIp, userAgent); // Reason에 LOCKED_ACCOUNT가 없으면 추가 필요 | ||||||||||
| throw new CustomException(GlobalExceptionCode.ACCOUNT_LOCKED); // GlobalExceptionCode에 추가 필요 | ||||||||||
|
Comment on lines
+93
to
+94
|
||||||||||
| saveLoginHistory(user, false, Reason.LOCKED_ACCOUNT, clientIp, userAgent); // Reason에 LOCKED_ACCOUNT가 없으면 추가 필요 | |
| throw new CustomException(GlobalExceptionCode.ACCOUNT_LOCKED); // GlobalExceptionCode에 추가 필요 | |
| saveLoginHistory(user, false, Reason.LOCKED_ACCOUNT, clientIp, userAgent); | |
| throw new CustomException(GlobalExceptionCode.ACCOUNT_LOCKED); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 129 "// Reason.WRONG_PASSWORD 확인 필요" (needs verification) should be removed. The Reason.WRONG_PASSWORD enum value already exists in the codebase, so this verification comment is unnecessary and can be deleted.
| saveLoginHistory(user, false, Reason.WRONG_PASSWORD, ip, userAgent); // Reason.WRONG_PASSWORD 확인 필요 | |
| saveLoginHistory(user, false, Reason.WRONG_PASSWORD, ip, userAgent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이제 주석이 불필요해진 것 같네요. 주석 삭제하셔도 될 것 같습니다.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,19 @@ | ||||||||||||||
| package until.the.eternity.das.login.entity; | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| import jakarta.persistence.*; | ||||||||||||||
| import lombok.*; | ||||||||||||||
| import jakarta.persistence.Column; | ||||||||||||||
| import jakarta.persistence.Entity; | ||||||||||||||
| import jakarta.persistence.FetchType; | ||||||||||||||
| import jakarta.persistence.Id; | ||||||||||||||
| import jakarta.persistence.JoinColumn; | ||||||||||||||
| import jakarta.persistence.MapsId; | ||||||||||||||
| import jakarta.persistence.OneToOne; | ||||||||||||||
| import jakarta.persistence.Table; | ||||||||||||||
| import lombok.AccessLevel; | ||||||||||||||
| import lombok.AllArgsConstructor; | ||||||||||||||
| import lombok.Builder; | ||||||||||||||
| import lombok.Getter; | ||||||||||||||
| import lombok.NoArgsConstructor; | ||||||||||||||
| import org.hibernate.annotations.Comment; | ||||||||||||||
| import until.the.eternity.das.user.entity.User; | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -17,26 +27,46 @@ | |||||||||||||
| @Builder | ||||||||||||||
| public class AccountLock { | ||||||||||||||
|
|
||||||||||||||
| @Id | ||||||||||||||
| @Column(name = "user_id") | ||||||||||||||
| @Comment("사용자 ID") | ||||||||||||||
| private Long userId; | ||||||||||||||
| @Id | ||||||||||||||
| @Column(name = "user_id") | ||||||||||||||
| @Comment("사용자 ID") | ||||||||||||||
| private Long userId; | ||||||||||||||
|
|
||||||||||||||
| @OneToOne(fetch = FetchType.LAZY) | ||||||||||||||
| @MapsId | ||||||||||||||
| @JoinColumn(name = "user_id") | ||||||||||||||
| private User user; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "failed_attempts", nullable = false) | ||||||||||||||
| @Builder.Default | ||||||||||||||
| @Comment("실패 시도 횟수") | ||||||||||||||
| private Integer failedAttempts = 0; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "locked_until") | ||||||||||||||
| @Comment("잠금 해제 예정 시각") | ||||||||||||||
| private LocalDateTime lockedUntil; | ||||||||||||||
|
|
||||||||||||||
| @OneToOne(fetch = FetchType.LAZY) | ||||||||||||||
| @MapsId | ||||||||||||||
| @JoinColumn(name = "user_id") | ||||||||||||||
| private User user; | ||||||||||||||
| @Column(name = "updated_at", nullable = false) | ||||||||||||||
| @Comment("최근 업데이트 시각") | ||||||||||||||
| private LocalDateTime updatedAt; | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "failed_attempts", nullable = false) | ||||||||||||||
| @Builder.Default | ||||||||||||||
| @Comment("실패 시도 횟수") | ||||||||||||||
| private Integer failedAttempts = 0; | ||||||||||||||
| // 실패 횟수 증가 및 잠금 처리 로직 | ||||||||||||||
| public void increaseFailAttempts() { | ||||||||||||||
| this.failedAttempts++; | ||||||||||||||
| this.updatedAt = LocalDateTime.now(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @Column(name = "locked_until") | ||||||||||||||
| @Comment("잠금 해제 예정 시각") | ||||||||||||||
| private LocalDateTime lockedUntil; | ||||||||||||||
| // 계정 잠금 설정 | ||||||||||||||
| public void lockAccount() { | ||||||||||||||
| this.lockedUntil = LocalDateTime.now() | ||||||||||||||
| .plusMinutes(5); | ||||||||||||||
| this.updatedAt = LocalDateTime.now(); | ||||||||||||||
|
Comment on lines
+61
to
+63
|
||||||||||||||
| this.lockedUntil = LocalDateTime.now() | |
| .plusMinutes(5); | |
| this.updatedAt = LocalDateTime.now(); | |
| LocalDateTime now = LocalDateTime.now(); | |
| this.lockedUntil = now.plusMinutes(5); | |
| this.updatedAt = now; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package until.the.eternity.das.login.entity; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface AccountLockRepository extends JpaRepository<AccountLock, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package until.the.eternity.das.login.entity; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface LoginHistoryRepository extends JpaRepository<LoginHistory, Long> { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The workflow file change from
devtomainbranch appears unrelated to the account locking feature described in the PR title and description. This change affects deployment triggers and should ideally be in a separate PR or explicitly mentioned in the PR description to explain why it's included.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dev에 PR시에는 개발서버, main에 PR시에는 운영서버로 배포가 될 예정이니 workflows 파일을 분리해서 deploy-dev, deploy-main 등으로 관리하면 좋을 것 같습니다.