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 |