1. 이메일 차단 기능
연속 5회 이상 로그인 실패 시 해당 이메일의 로그인을 차단하는 기능을 만들었다. ttl은 꽤 길게 잡았다
잘못된 비밀번호를 입력하면 사진처럼 이메일을 키로, 횟수를 밸류로 레디스에 저장한다.
ttl이 긴 만큼 로그인 성공 시 fail키는 사라진다.
이렇게 5번 잘못입력하면 blocked를 가진채로 저장되며
해당 이메일로 로그인 시도 시 차단된다.
이메일 차단 기능 코드
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class EmailBlockingAspect {
@Around("execution(* 로그인 컨트롤러 위치(..))")
public Object blockAbnormalEmail(ProceedingJoinPoint joinPoint) throws Throwable {
//차단된 이메일인지 확인
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
log.warn("임시차단된 사용자의 로그인 요청: {}", email);
redisTemplate.opsForValue().set(redisKey, "blocked", EMAIL_BLOCK_DURATION, TimeUnit.MINUTES);
throw new UnAuthorizedException(AuthErrorCode.BLOCKED_EMAIL);
}
try {
Object result = joinPoint.proceed(args);
//로그인 성공 시 failCount 삭제
redisTemplate.delete(failCountKey);
return result;
} catch (Exception e) {
if(Objects.equals(e.getMessage(), "패스워드가 잘못되었습니다.")){
log.error("로그인 실패: {}", email);
//잘못된 비밀번호 입력 시 count 1회 추가, 첫 추가 시 ttl 설정
long failedAttempts = redisTemplate.opsForValue().increment(failCountKey);
if (failedAttempts == 1) {
redisTemplate.expire(failCountKey, EMAIL_FAIL_DURATION, TimeUnit.DAYS);
}
//잘못된 시도 5회 시 이메일 차단
if (failedAttempts >= MAX_FAILED_COUNT) {
redisTemplate.opsForValue().set(redisKey, "blocked", EMAIL_BLOCK_DURATION, TimeUnit.MINUTES);
log.warn("임시차단된 이메일: {} 이 {} 분 간 차단되었습니다", email, EMAIL_BLOCK_DURATION);
redisTemplate.expire(failCountKey, EMAIL_FAIL_DURATION, TimeUnit.MINUTES);
}
}
throw e;
}
}
}
(변수와 생성자 선언하는 부분 코드는 안 가져왔다)
로그인 컨트롤러메서드를 around로 감싸서 서비스에 넘어가기 전에 이메일을 차단하도록 했다
차단된 이메일인지 확인하고
1. 로그인 성공 시 failCount를 삭제
2. 로그인 실패 시("패스워드가 잘못되었습니다" 와 에러메세지가 같은 경우 << 근데 이 부분 보안 관점에서 괜찮은걸까 생각하게된다)
count를 1회 추가하며, 첫 추가(첫 카운트 갱신)시에 ttl을 설정한다
count가 일정 횟수를 넘기면 해당 이메일에 대한 로그인을 막는다.
확장가능성:
차단된 email은 db에서 관리하고, 이메일 인증을 통해 밴을 풀 수 있다.
관리자와 1대1 채팅을 통해 밴을 풀어달라고 조를 수도 있을 것이다.
관리자 페이지로 확장도 가능하다.
2. ip 차단 기능
로그인을 시도할 때 마다 사용자의 ip를 key로, 시도한 이메일을 list의 요소로 가지도록 구현했다.
이 값은 로그인에 성공해도 사라지지 않도록 했다. 성공해도 갱신되는 값이다
매 시도마다 ttl이 새로 갱신된다.
한 ip에서 로그인 시도한 이메일의 개수가 정해진 값을 초과하면 차단됐다는 로그와 함께, blocked ip로 등록된다.
로그인 시도 시 차단된다.
ip 차단 기능 코드
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class IpBlockingAspect {
@Around("execution(* 로그인 컨트롤러 위치(..))")
public Object blockAbnormalIp(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = getRequest();
if (request == null) {
return joinPoint.proceed();
}
//차단된 ip 인지 확인
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisBlockKey))) {
log.warn("차단된 IP 로그인 시도: {}", ip);
throw new UnAuthorizedException(AuthErrorCode.BANNED_IP);
}
try {
return joinPoint.proceed();
} catch (Exception e) {
log.error("로그인 실패: IP={}, 이메일={}", ip, email);
//해당 ip 에서 로그인 시도한 이메일 리스트 가져오기
List<String> attemptedEmails = redisTemplate.opsForList().range(redisAttemptKey, 0, -1);
if (attemptedEmails == null || !attemptedEmails.contains(email)) {
redisTemplate.opsForList().rightPush(redisAttemptKey, email);
}
//추가 시 ttl 설정
redisTemplate.expire(redisAttemptKey, ATTEMPT_TTL, TimeUnit.MINUTES);
//서로 다른 이메일이 3개 이상이면 차단
if (!Objects.requireNonNull(attemptedEmails).contains(email) && attemptedEmails.size() >= MAX_DIFFERENT_EMAILS) {
redisTemplate.opsForValue().set(redisBlockKey, "blocked", IP_BLOCK_DURATION, TimeUnit.DAYS);
log.warn("IP {} 차단됨 (서로 다른 {}개 이메일 감지됨)", ip, MAX_DIFFERENT_EMAILS + 1);
}
throw e;
}
}
private HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest() : null;
}
private String getClientIp(HttpServletRequest request) {
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
ip = ip.split(",")[0].trim();
} else {
ip = request.getRemoteAddr();
}
if (ip.contains(":")) {
log.info("IPv6 추출 성공: {}", ip);
} else {
log.info("IPv4 추출 성공: {}", ip);
}
return ip;
}
}
(변수 생성자 선언하는 부분 코드는 안 가져왔다)
마찬가지로 컨트롤러를 감쌌다.
RequestContextHolder로 ip를 추출한다. 변별력을 위해 ipV6을 우선적으로 가져오고, 없다면 ipV4로 가져온다
ip를 key로, value를 리스트<이메일>로 해서 사용자의 로그인 시도를 감지해서, 서로 다른 이메일이 정해진 개수를 넘어가면 해당 ip를 차단한다.
확장가능성:
차단된 ip역시 mysql로 넘길 수 있다.
ip밴은 인터셉터를 활용해서 어플리케이션 전역에 활용하면 좋을거다. 구현해야지
좀 더 생각중인 기능:
이미 로그인된 사용자의 이메일로 로그인 하는 경우? token기반으로 어떻게 구현할 수 있을까
jwt payload 암호화
'팀 프로젝트 > cheerha.project' 카테고리의 다른 글
ec2 내부 도커 컨테이너에서 prometheus + grafana 사용하기 (0) | 2025.02.20 |
---|---|
배포(완) RDS, ElastiCache 인프라 구축 (0) | 2025.02.17 |
배포(3) 무중단 배포: 롤링 vs 블루-그린(https 배포완료) (0) | 2025.02.16 |