bbs-basic-6, ip 차단, Handler Interceptor

김지원·2022년 6월 18일
0

WebDevelop

목록 보기
7/21

ILLEGAL이나 FAILURE 결과 값이 5개 이상 나왔다면 ip차단을 해보자.

주어진 시간에 시간을 더하는 함수

SELECT DATE_ADD(NOW(), INTERVAL 10 SECOND);
SELECT DATE_ADD(NOW(), INTERVAL 10 MINUTE );
SELECT DATE_ADD(NOW(), INTERVAL 10 DAY);
QUARTER, YEAR
SELECT DATE_SUB(NOW(), INTERVAL 1 YEAR);
  • DATE_ADD(x, INTERVAL y z :x 시간에 y(양) z(단위)만큼 추가
  • 단위는 SECOND, MINUTE, HOUR, DAY, WEEK, MONTH,

-> system_banned_ips 테이블 생성
차단 시킬 ip를 가지고 있는 테이블

CREATE TABLE `spring3`.`system_banned_ips`
(
    `index`        BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `created_at`   DATETIME        NOT NULL DEFAULT NOW(),
    `expires_at`   DATETIME        NOT NULL,
    `expired_flag` BOOLEAN         NOT NULL DEFAULT NOW(),
    `ip`           VARCHAR(50)     NOT NULL,
    CONSTRAINT PRIMARY KEY (`index`)
);
  • 어떤 ip를 차단하겠다는 규칙
    created_at : 이 규칙이 언제 만들어졌고,
    expires_at : 언제까지 유효하며,
    expired_flag : 현재 유효한지에 대한 여부,
    ip : 차단할 ip

-> SystemBannedIpEntity Entity 생성

private long index;
private Date CreatedAt;
private Date expiresAt;
private boolean isExpired;
private String ip;

차단할지 말지에 대한 여부는 언제 지정하는 것인가?
ip를 차단시키는 기준 : 지난 일부시간을 봤을 떄 FALURE, ILLEGAL Count를 세서 특정한 수를 초과하면 차단이 된다.
그 여부는 아래의 SystemService putActivityLog 메서드에서 진행이 된다.

-> SystemService

  • boolean 타입인checkPast 과거를 확인하라는 것. = 기본값을 true로 둔다.
  • chackActivityAndVBan 메서드 추가.

ip 차단

chackActivityAndVBan메서드에서는 전달받은 request가 가지고 있는 ip주소를 활용해서 이 ip주소가 지난 몇분 또는 몇시간 동안 몇번의 ILLEGAL 또는 FAILURE을 발생시켰는지 확인하고 그 임계값을 넘어섰다면 banned_ips에다가 insert를 시키는 방식으로 진행을 해보자.

-> ISystemMapper 추가

int insertBannedIp(SystemBannedIpEntity systemBannedIpEntity);
int selectBadActivityCountByIp(
       @Param(value = "ip") String ip,
       @Param(value = "lookBackSeconds") int looBackSeconds);

lookBackSeconds 어떠한 ip가 지난 몇초동안 저질렀던 나쁜 행위에 대한 갯수를 가져온다. 시간에 대한 조건 횟수제한.

-> SystemMapper.xml selectBadActivityCountByIp select 추가

<select id="selectBadActivityCountByIp"
            resultType="_int">
        SELECT COUNT(0)
        FROM `spring3`.`system_activity_logs`
        WHERE `client_ip` = #{ip}
        AND `result` IN ('FAILURE', 'ILLEGAL')
        AND `created_at` > DATE_SUB(NOW(), INTERVAL #{lookBackSecond} SECOND)
</select>

AND `result` IN ('FAILURE', 'ILLEGAL')
=> reulst 가 FAILURE이거나 ILLEGAL이거나 라는 의미

AND `created_at` > DATE_SUB(NOW(), INTERVAL #{lookBackSecond} SECOND)

현재시간에서 전달받은 lookBackSecond만큼 뺀 값보다 일시가(= created_at의 값이) 더 큰 레코드를 선택해야한다.

이해를 위해서 예시를 들자.

어떤 누군가가 무언가를 저질러서 FAILURE가 떴다고 하자.
그때 checkPast가 true가 되면
SystemService의 public void putActivityLog 메서드에서

if(checkPast) {
	this.checkActivityAndBan(request);
}

이 아이가 실행이 될 것이다. 이게 실행이 될때 lookBackSecondip의 값이 들어왔을 것이다. 이때 조건이 아래에 것에 다 해당이 된다.

WHERE `client_ip` = #{ip}
AND `result` IN ('FAILURE', 'ILLEGAL')
AND `created_at` > DATE_SUB(NOW(), INTERVAL {lookBackSecond} SECOND)

이때 갯수를 세서 3개를 초과한다면 차단한다고 치자. 근데 이렇게 차단시키면 안된다. "지난 시간동안" 갯수가 3개를 초과한다면 차단을 시키는 것, 시간을 정해놓는 것이 옳은 것이다.

현재시간에서 나쁜짓을 저지른 시간을 뺴고 그것보다 더 큰 created_at 레코드만 가져와서 갯수를 비교하는 로직을 짠 것이다 .
DATE_SUB(NOW(), INTERVAL 600 SECOND) 한 것이랑 createAt field랑 비교해서 들어온값이랑 비교하는 로직.


-> SystemService

public static final int BAD_ACTIVITY_LOOK_BACK_SECONDS = 600; //상수화
public static final int BAD_ACTIVITY_LIMIT = 5;

맨위에 추가.

-> SystemService, checkActivityAndBan 추가

public void checkActivityAndBan(HttpServletRequest request){
        int badActivityCount = this.systemMapper.selectBadActivityCountByIp(request.getRemoteAddr(),BAD_ACTIVITY_LOOK_BACK_SECONDS);
        if(badActivityCount >= BAD_ACTIVITY_LIMIT) {
        // 차단
        }

5번이 되는 순간 차단이 된다.


=> 차단을 매길때 지난 차단 이력을 보고 그 ip가 전과범이면 과중 처벌을 할 것이다.
-> ISystemMapper selectBannedIpCountByIpAll 추가

 int selectBannedIpCountByIpAll(
                @Param(value = "ip") String ip);

-> SystemMapper select 추가

<select id="selectBannedIpCountByIpAll"
        resultType="_int">
    SELECT COUNT(0)
    FROM `spring3`.`system_banned_ips`
    WHERE `ip` = #{ip}
</select>
  • 조건없이 갯수만 돌려준다.
  • 이때까지 한번도 차단되지 않은 ip면 0이 나올테고
    악질적인 ip는 그 이상이 나오게 된다.

차단해야하는 영역!!!
-> SystemService 맨위에 추가

public static final int BAN_MINUTES =  10;

-> SystemService, checkActivityAndBan 5번 이상 일때의 차단 if문 추가

 	public void checkActivityAndBan(HttpServletRequest request){
        int badActivityCount = this.systemMapper.selectBadActivityCountByIp(request.getRemoteAddr(),BAD_ACTIVITY_LOOK_BACK_SECONDS);
        if(badActivityCount >= BAD_ACTIVITY_LIMIT) {
            //차단
            int bannedTimes = this.systemMapper.selectBannedIpCountByIpAll(request.getRemoteAddr()) + 1 ;
            int banMinutes = (int)(BAN_MINUTES * Math.pow(bannedTimes, bannedTimes));
            if(banMinutes < 0) {
                banMinutes = Integer.MAX_VALUE;
            }
            Date createAt = new Date();
            SystemBannedIpEntity systemBannedIpEntity = new SystemBannedIpEntity();//객체화
            systemBannedIpEntity.setCreatedAt(createAt);
            systemBannedIpEntity.setExpiresAt(DateUtils.addMinutes(createAt, banMinutes));
            systemBannedIpEntity.setExpired(false);
            systemBannedIpEntity.setIp(request.getRemoteAddr());
            this.systemMapper.insertBanedIp(systemBannedIpEntity);
        }
    }
  • 차단 당한 횟수를 센다.
int bannedTimes = this.systemMapper.selectBannedIpCountByIpAll(request.getRemoteAddr());

기본 10분이고 차단 당한 적 한번도 없다면 request.getRemoteAddr()의 select결과가 0이다. 여기에 +1을 해준다. (if문이 통과가 되었기 때문임)

  • 차단 시간
    int banMinutes = (int)(BAN_MINUTES * Math.pow(bannedTimes, bannedTimes));
초범 - 10 * (1 ^ 1) = 10 -> 10분차단 
2범 - 10 * (2 ^ 2) = 40 -> 40분차단 
3범 - 10 * (3 ^ 3) = 270 -> 270분차단 
4범 - 10 * (4 ^ 4) = 2560 -> 2560분차단 

1의 1승이니깐 초범은 10분차단이다.
이런식으로 계속 늘어나면 INT(최대 20억~) 오버플로우 발생할 수 있음. 즉 사용자가 아주 아주 많이 죄를 지으면 차단을 당하지 않는다는 의미이다. 20억을 넘어버리느면 오류가 발생하지는 않고 마이너스로 들어가게 된다.

if(banMinutes < 0) {
	banMinutes = Integer.MAX_VALUE;
}
  • 그렇기에 조건식을 걸어준다.

-> SystemMapper < insert > 쿼리 추가

<insert id="insertBannedIp"
        parameterType="dev.jwkim.bbsbasic.entities.SystemBannedIpEntity">
        INSERT INTO `spring3`.`system_banned_ips` (`created_at`, `expires_at`, `expired_flag`, `ip`)
        VALUES (#{createdAt}, #{expiresAt}, #{isExpired}, #{ip})
</insert>

로그인을 5번정도 실패로 하고 banned_ips에서 봤을 때 ip가 차단이 되면 된다.
아직 정상작동은 할 것이다. 이 테이블에 들어있다고 해서 이 ip를 차단한다는 로직은 짜지 않았기 때문이다.

xml에서 insert, update 든 뭘 하든 가용성을 높이려면 쿼리에 index를 제외한 나머지를 모두 명시해주는 것이 좋다. default라고 나두면 나중에 따로 쿼리를 짜줘야하는 일이 발생할 수도 있기 때문이다.

여기까지 짰으면 나쁜사람인지 확인하고 차단을 하는 것까지 한것이다 .


실제로 차단을 당한사람이 차단을 당한지 알지 못한다. 지금부터 해보자.
-> SystemService isIpBanned 메소드 추가

-> ISystemMapper 인터페이스

 int selectBannedIpCountByIp(
            @Param(value = "ip") String ip);

이 두개의 차이
selectBannedIpCountByIpAll : 현재 만료가 되었든 안되었던 지난 이력을 다 보는 것.
selectBannedIpCountByIp : 현재 유효한 것만 본다. (아래의 조건에 따라)
`expires_at` > NOW() / `expired_flag` = FALSE 인 것을 골라올 것이다.

=> isIpBanned 메서드에 의해서 반환된 결과값이 0이라면 그 사람은 차단을 당하지 않은 것이고 0을 초과하면 차단을 당한사람이다.

-> SystemMapper.xml

<select id="selectBannedIpCountByIp"
            resultType="_int">
        SELECT COUNT(0)
        FROM `spring3`.`system_banned_ips`
        WHERE `ip` = #{ip}
        AND `expires_at` > NOW()
        AND `expired_flag` = FALSE
</select>

만료되는 일자가 지금보다 미래일것. 만료되지 않은 규칙만 가지고 온다.

-> SystemService isIpBanned 메서드 추가

  • 차단을 당한사람을 돌려준다.

-> banned.html 생성


-> RootController

  • StandardController 상속받고 생성자 만들고 @Autowired, 생성자의 접근제한자를 public으로 열어놓는다.

-> RootController getIndex

 @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView getIndex(
            ModelAndView modelAndView
    ) {
        if(this.systemService.isIpBanned(request)) {
            modelAndView.serViewName("banned");
            return modelAndView;
        }
        modelAndView.addObject("templateTitle", "메인 페이지");
        // 뷰페이지 이름을 templateTitle, 값을 "메인 페이지" 로 설정.
        modelAndView.addObject("templateMain","root/index");
        // 뷰페이지 이름을 templateMain, 값을 root/index 로 설정
        modelAndView.setViewName("template");
        // 모든 컨트롤러에서 뷰 네임은 template.html / 이렇게 이동하고자 하는 view 저장한다.
        return modelAndView;
//       뷰이름의 명시적 지정 : ModelAndView 나 String 리턴해야한다.

    }
  if(this.systemService.isIpBanned(request)) {
            modelAndView.serViewName("banned");
            return modelAndView;
        }

-> 추가 ( 삭제 하고 다른 거 사용 예정 )
이 사람이 차단을 당한 상태인지 어떤지 확인을 하기 위해 request는 항상 필요하다. this.systemService.isIpBanned(request) 얘가 banned 상태 = 차단된 상태

=> 이렇게 짜고 차단된 상태에서 메인페이지로 들어갔다면 차단당함 이라고 뜰 것이다.

그런데 userResigister은 들어가진다. 왜냐하면 UserController register 맵핑에도 동일한 내용을 적어야 차단을 하던가 말던가 되기 때문이다.

  • 느낌표 있는 자리에 if문.

이렇게 일일이 추가 해줄 수도 있긴 하지만 차단에 대한 모든 경로의 맵핑에 대해서 전부 이것을 추가해 줄 수 없기 때문에 ( 귀찮기 때문에 ) intercepter을 사용한다.
------ if문 삭제 -------


핸들러 인터셉터(Handler Interceptor)

클라이언트의 요청이 컨트롤러에 가기 전에 가로채고, 응답이 클라이언트에게 가기전에 가로챈다. 즉, 인터셉터는 DispatcherServlet이 컨트롤러를 요청하기 전,후에 요청과 응답을 가로채서 가공할 수 있도록 해준다.

< 장점 >

  • 공통 코드 사용으로 코드 재사용성 증가
  • 메모리 낭비, 서버 부하 감소
  • 코드 누락에 대한 위험성 감소

인터셉터를 만들려면 HandlerInterceptorAdaptor 클래스를 상속받아야 한다. HandlerInterceptorAdaptor 클래스를 상속받으면 사용할 수 있는 3가지의 메서드들이 있다. perHandle(), postHandle(), afterHandel()이 있다.

  • preHandle(request, response, handler)
    : 지정된 컨트롤러의 동작 이전에 수행할 동작 (사전 제어).
  • postHandle(request, response, handler, modelAndView)
    : 지정된 컨트롤러의 동작 이후에 처리할 동작 (사후 제어).
    Spring MVC의 Dispatcher Servlet이 화면을 처리하기 전에 동작.
  • afterCompletion(request, reponse, handler, exception)
    : Dispatcher Servlet의 화면 처리가 완료된 이후 처리할 동작.

예를 들어 로그인 기능이 있을 때, 로그인을 한 사람만 보이는 페이지가 있고, 로그인 한 사람만 글을 작성할 수 있다고 하자. 그러면 페이지 컨트롤러에서도 로그인 확인 로직이 들어가고, 글 작성 컨트롤러에서도 로그인 확인 로직이 들어가야 한다. 인터셉터를 사용하면 컨트롤러에 로직이 로그인 확인 로직이 없어도 컨트롤러에 들어가기전에 인터셉터에서 로그인 확인을 하고 컨트롤러로 보낸다. 즉, 하나의 인터셉터로 프로젝트 내의 모든 요청에 로그인 여부를 확인할 수 있다. 

출처: https://to-dy.tistory.com/21 [todyDev:티스토리]

Controller로 들어가기 전에 우리가 지정한 주소 패턴에 따라서 앞에 장막을 만들어 놓는 것.

  • Interceptor은 Controller로의 걔속된 진행을 허가하거나 아니면 거절 할 수 있다.
  • 세션처리나 보안처리 등에 이용한다.

-> Interceptors 패키지 - SystemInterceptor 생성

  • implements HandlerInterceptor인터페이스를 구현한다.
    구현을 하게 되면 SystemInterceptor은 클래스가 아니라 인터셉터가 된다.
  • 인터페이스를 구현을 했는데 왜 오류가 안떴을까?

    ctrl + o 재정의 할 수 있는 메서드가 뜨는데 그 중 preHandle선택.
  • preHandle은 Controller로 넘기기전에 취해야하는 조치들을 취하는 것.
    postHandle Controller까지 갔다가 다시 나올 때, 요청이 들어오게 되면 Controller보다 interceptor이 먼저 받는다.

요청이 들어오고
interceptor -> Controller 보내기 전 실행하는 게 preHandle
Controller에서 필요한 조치를 다 취하고 빠져나갈 때 실행하는 게 postHandle

preHandle메서드는 boolean 타입을 반환한다.
boolean 타입이 ture면 controller로 진행하고 false면 controller로 진행하지 않는다.
그래서 보안 결격사유가 발생하면 return false를 해주면 된다.


구조상 SystemInterseptor은 @Autowired 쓸 수 있긴한데 생성자를 통한 @Autowired은 사용하지 못한다. 그래서 final도 아닌 private만으로 추가.

private SystemService systemService;

  • 우리가 직접 객체화하지 못한다. 결론적으로 Autowired로 객체화가 되어야하는데 생성자가 아닌 멤버변수 자체에 @Autowired가 붙어있으면 된다.

그 다음 preHandle : controller로 넘기기전에 해줘야 행위는 request는 자동으로 있고 우리가 아는 HttpSevletRequest이다.
SystemService 해당 ip가 차단되었는지에 대한 여부를 확인하는 메서드를 잘 만들어놨다. 사용해서 코드를 쓸 것이다.

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(this.systemService.isIpBanned(request)) {
            response.sendRedirect("/banned");
            return false; // false => controller로 안넘어간다.
        }
        return true;
    }

if(this.systemService.isIpBanned(request))
만약 ip가 차단된 것으로 확인이 되면 return false; Controller로 넘기지 않는다.
그렇게 되면 흰 화면만 보게 되기 때문에 responsesendRedirect메서드를 이용해 "/banned"이쪽으로 redirect시켜서 사용자가 차단 당했다는 사실을 알려준다.
그 외의 경우 ip가 차단당한게 아니면 모두 return true; 로 정상적으로 진행해라 라고 해준다.


지금 상태로는 등록이 되지 않았다. interceptor은 모든 주소에 작동하지 않고 특정 주소 패턴에 대해서 광범위하게 작동을 한다. 그래서 등록을 시켜줘야한다.
-> configs 패키지 webMvcConfig클래스 생성

  • 얘는 설정 파일이다 : @Configuration 어노테이션 사용.
  • WebMvcConfigurer 인터페이스 구현.
  • ctrl + o addInterceptors 메서드 추가

public class WebMvcConfig implements WebMvcConfigurer
얘를 spring이 직접 객체화하게 만들게 하자.
SystemInterceptor을 반환하는 메서드 만들고 return new SystemInterceptor(); 까지 해놓아도 우리가 직접 객체화 한것이다. 그래서 가지고 와도 객체화가 되어있지 않다.
=> 스프링에게 시켜야한다.

-> webMvcConfig 클래스

  • spring 니가 객체화해서 줘야한다는 의미로 @Bean어노테이션 추가.

-> SystemInterceptor 확인

  • Bean이 없으면 Autowired에 경고들어온다. : 스프링이 인식가능한 범위내에 있는 클래스여야 Autowired가 가능한다. 지금은 인식할 수 없다는 것이다.

-> webMvcConfig addInterceptors 메서드 추가

.addPathPatterns("/**") : 이러한 경로 패턴에는 해당 인터셉터가 작동했으면 좋겠다라는 경로 패턴을 적는다. -> 모든 경로에 대해서 ip체크를 한다

예외가 있다!

차단은 당하더라도 banned된 것을 볼 수 있어야 한다. 아니면 무한루프가 돌아버리게 된다.
.excludePathPatterns("/banned") -> 차단 당하면 이주소로 빠지게 된다.

.excludePathPatterns("/resources/**")
시스템 과부하를 막기위해 resources로 시작하는 모든 것들을 예외로 해준다.

현재 어느 주소로 들어가든 차단된 상태이기 때문에
http://localhost:8080/banned 로 빠지게 된다.
왜냐하면 Controller에 도달을 하지 못했기 때문에 그 맵핑이 있는지 없는지 조차 모른다. interceptor한테 막혔다. 말 그대로 모든 주소에 대해서 주소가 차단당해서 banned로 빠진다.

-> RootController

  • 오류를 발생시키는 임의의 메서드 getRaiseError 작성.

-> Standard controller handleException 메서드 생성

  • 접근성이 떨어지고 시스템의 예외적인 오류같은 내용들을 당연히 노출시키면 안된다. 그랴서 모든 맵핑에 대해 try Catch에서 catch에 걸리는 modelAndView에 이름을 바꾸고 return 해주고 그럴수가 없기 때문에 interceptor와 느낌만 비슷하게 전역적으로 처리를 해주는게 좋다.
	@ExceptionHandler(value = Exception.class)
    protected ModelAndView handleException(Exception exception) {
        try{
            System.out.println(exception.getMessage());
            exception.printStackTrace();

            ModelAndView modelAndView = new ModelAndView("error");
            return modelAndView;
        } catch (Exception ignored) {
            return null;
        }
    }
  • Standard controller를 상속받는 모든 Controller 에서 발생하는 혹은 그 이하 서비스나, 모델, 맵퍼에서 발생하는 모든 예외는 얘가 싹다 처리한다그러기 위해서 @ExceptionHandler 어노테이션을 추가한다.
    value = Exception.class
    -> 이 메서드는 이러한 예외를 처리한다라고 value를 설정할 수 있다. 지금은 통합해서 Exception라고 적고 type of class 해줘야하기 때문에 .class로 적어준다.

  • handleException 메서드는 모든 예외에 대한 최후이기 때문에 handleException안에서는 예외가 밖으로 새어나가면 안된다. 무한루프로 돌지 않기 위해서 이 안에서 조취하는 모든 오류는 이 안에서 끝나야한다.

System.out.println(exception.getMessage()); exception.printStackTrace(); 을 통해서 어떤 오류가 발생했는지 본다.

-> error.html

  • 오류페이지 느낌만 주자
  • 아무렇게 치면 에러 뜬다.
  • 없는 맵핑으로 들어가면 404로 뜬다. (예외는 아니다.)

전역적인 오류 처리를 위해서 존재하는 내용들이다.

ExeptionLogs 라는 테이블에 insert해줘야한다.


-> SystemService putExceptionLog 메서드 추가

public void putExceptionLog(Exception exception) {
        String message = exception.getMessage();
        String stacktrace = ExceptionUtils.getStackTrace(exception);
}

의존성 DateUtiles 추가한것에 ExceptionUtils가 있다. 사용하자.
(1:14 원래 방법)

Entity 만들어서 객체화하자.
-> ExceptionLogEntity 추가

public class ExceptionLogEntity {
    private long index;
    private Date createdAt;
    private String message;
    private String stacktrace;
    getter..setter..

-> SystemSerivce

  • 객체화해준다.

-> ISystemMapper

  • int insertExceptionLog(SystemExceptionLogEntity systemExceptionLogEntity); 추가

-> SystemMapper.xml

-> StandardController

 this.systemService.putExceptionLog(exception); 추가

-> SystemService

this.systemMapper.insertExceptionLog(systemExceptionLogEntity); 추가

Exeption을 그냥 던졌더니 getMessage가 null을 던져서 조취를 취하자.

  • 이걸 자바스크립트에서 표현을 했을 때는 ?? "" 이런식으로 했다.

중복로그인 차단은 하지 않았다.

추가 )
MVC 패턴으로 보면
model 안에 dao, service가 있는 것처럼.
controller 그룹 안에 controller 와 interceptor이 있는 것이다.

profile
Software Developer : -)

0개의 댓글