로그인 횟수 제한 구현

2024. 12. 28. 22:20·토이 프로젝트 2

1. LoginController

@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginDto") LoginDto loginDto, BindingResult result, HttpServletRequest request, HttpServletResponse response, Model model
) throws Exception {
    log.debug("Received LoginDto: {}", loginDto);
    log.debug("Email: {}, Password: {}", loginDto.getEmail(), loginDto.getPassword());


    model.addAttribute("loginDto", loginDto);

    //1. 입력을 제대로 했는지 부터 확인
    if (result.hasErrors()) {
        log.warn("Validation errors: {}", result.getAllErrors());
        return "login";
    }
    //2. 제대로 입력했으면 잠금상태 확인
    if (loginAttemptService.isAccountLocked(loginDto.getEmail())) {
        result.reject("loginFail", "계정이 잠겨있습니다. 잠시 후 다시 시도해주세요.");
        model.addAttribute("errorMessage", "계정이 잠겨있습니다. 잠시 후 다시 시도해주세요."); // 에러 메시지 추가
        log.warn("로그인 차단: 계정 {} 잠금 상태", loginDto.getEmail());
        return "login";
    }

    //3. 잠기지 않았으면 로그인 시도
    try {
        Member loginMember = loginService.login(loginDto.getEmail(), loginDto.getPassword());
        loginAttemptService.resetAttempts(loginDto.getEmail()); // 로그인 성공했으니까 리셋.
        //4. 로그인 성공했으면 세션 생성
        HttpSession session = request.getSession();
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        if (loginMember.getMemberType() != null) {
            session.setAttribute("memberType", loginMember.getMemberType());
        }

        log.info("로그인 성공: 세션 생성, 세션 ID: {}", session.getId());
        //5. 쿠키도 설정
        Cookie loginCookie = new Cookie("loginMember", URLEncoder.encode(loginMember.getEmail(), "UTF-8"));
        loginCookie.setHttpOnly(true);
        loginCookie.setPath("/");
        loginCookie.setMaxAge(60 * 60);

        response.addCookie(loginCookie);

        return "redirect:/";
        // 예외발생하면
    } catch (IllegalArgumentException e) {
        loginAttemptService.recordFailAttempt(loginDto.getEmail());

        int remainingAttempts = LoginAttemptService.MAX_ATTEMPTS - loginAttemptService.getAttempts(loginDto.getEmail());
        if (remainingAttempts > 0) {
            log.warn("로그인 실패: 사용자 {}, 남은 시도 횟수 {}", loginDto.getEmail(), remainingAttempts);
            model.addAttribute("errorMessage", "이메일 또는 비밀번호가 맞지 않습니다. 남은 시도 횟수: " + remainingAttempts);

            result.reject("loginFail", "이메일 또는 비밀번호가 맞지 않습니다. 남은 시도 횟수: " + remainingAttempts);
        } else {
            log.warn("계정 {}잠김", loginDto.getEmail());
            result.reject("loginFail", "계정이 잠겼습니다. 잠시 후 다시 시도해주세요,");
            model.addAttribute("errorMessage", "계정이 잠겼습니다. 잠시 후 다시 시도해주세요.");
        }
    }
    return "login";
}

 

2.LoginAttempt 엔티티(로그인의 횟수를 확인할 수 있는)

package com.example.bravobra.domain;


import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class LoginAttempt {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Builder.Default
    private int attempts = 0;

    private LocalDateTime lockedUntil;

    private LocalDateTime lastAttempt;

    // 실패 시 시도 횟수 증가
    public void increaseAttempts() {
        this.attempts += 1;
        this.lastAttempt = LocalDateTime.now();
    }

    // 잠금 상태 설정
    public void lockAccount(long lockTimeMinutes) {
        this.lockedUntil = LocalDateTime.now().plusMinutes(lockTimeMinutes);
    }

    public boolean isLocked(){
        return this.lockedUntil != null && this.lockedUntil.isAfter(LocalDateTime.now());
    }




}

 

3. LoginAttemptService

package com.example.bravobra.service;


import com.example.bravobra.domain.LoginAttempt;
import com.example.bravobra.repository.LoginAttemptRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class LoginAttemptService {

    public static final int MAX_ATTEMPTS = 3;
    public static final long LOCK_TIME_MINUTES = 10;

    private final LoginAttemptRepository loginAttemptRepository;

    @Transactional
    public void recordFailAttempt(String email) {
        LoginAttempt attempt = loginAttemptRepository.findByEmail(email)
                .orElse(LoginAttempt.builder()
                        .email(email)
                        .attempts(0)
                        .lastAttempt(LocalDateTime.now())
                        .build());

        //실패 시 횟수 증가
        attempt.increaseAttempts();

        // 실패 횟수 초과 시 계정 잠금
        if(attempt.getAttempts() >= MAX_ATTEMPTS) {
            attempt.lockAccount(LOCK_TIME_MINUTES);
        }
        loginAttemptRepository.save(attempt);
    }


    // 로그인 성공 시 시도 횟수 초기화.
    @Transactional
    public void resetAttempts(String email) {
        loginAttemptRepository.findByEmail(email).ifPresent(loginAttemptRepository::delete);
    }

    //현재 시도 횟수
    @Transactional(readOnly = true)
    public int getAttempts(String email) {
        return loginAttemptRepository.findByEmail(email)
                .map(LoginAttempt::getAttempts)
                .orElse(0);
    }


    @Transactional(readOnly = true)
    public boolean isAccountLocked(String email){
        return loginAttemptRepository.findByEmail(email)
                .map(attempt -> attempt.getLockedUntil() != null && attempt.getLockedUntil().isAfter(LocalDateTime.now()))
                .orElse(false);
    }
}

'토이 프로젝트 2' 카테고리의 다른 글

AdminIntercepter(Enum에 대한 오류 해결)  (0) 2024.12.29
비밀번호 찾기 구현  (1) 2024.12.27
Interceptor(권한) 구현  (1) 2024.12.26
커스텀 어노테이션 추가, ExceptionHandler 추가  (0) 2024.12.25
회원가입시 비밀번호 암호화(bCrypto)  (0) 2024.12.24
'토이 프로젝트 2' 카테고리의 다른 글
  • AdminIntercepter(Enum에 대한 오류 해결)
  • 비밀번호 찾기 구현
  • Interceptor(권한) 구현
  • 커스텀 어노테이션 추가, ExceptionHandler 추가
신댕인생
신댕인생
안녕하세요!! 예비개발자 신댕입니다. 개발자를 준비하고 있는 신댕인생의 하루하루를 정리하고 있어요. 파이팅!
  • 신댕인생
    신댕인생
    신댕인생
  • 전체
    오늘
    어제
    • 분류 전체보기 (127)
      • 다시 준비 (0)
        • 계획(ISTJ) (0)
        • 정보처리기사 (0)
        • SQLD (0)
        • Toeic Speaking (0)
        • 일일 회고 (0)
      • 데브캠프 일지 (52)
        • 예제 (6)
        • 연습문제 (12)
        • 과제 (2)
        • 칠판 정리 (1)
        • 숙지용 필기 (11)
      • 책 (12)
        • Clean Architecture (2)
        • 개발자도 궁금한 IT 인프라 (1)
        • JVM 밑바닥까지 파헤치기 (9)
      • DB (23)
        • 숙지용 필기 (12)
        • SQL 쿼리 실습 (7)
        • 극장 시스템 모델링 (3)
      • 토이 프로젝트 2 (7)
      • 토이 프로젝트 3 (0)
      • 뚜띠 (3)
      • 회고 (0)
        • 주간 회고 (0)
        • 월간 회고 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring
    sql의 정석
    연습문제
    jvm
    데브캠프
    쿼리 실습
    남궁성
    ERD
    부트캠프
    자바의 정석
    지네릭스
    JPA
    백엔드 부트캠프
    머신러닝 교과서
    패스트 캠퍼스
    패스트캠퍼스
    SQL
    패스트캠퍼스 부트캠프
    spring의 정석
    패스트캠퍼스 데브캠프
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
신댕인생
로그인 횟수 제한 구현
상단으로

티스토리툴바