토이 프로젝트/익명 채팅 사이트

프로젝트 진행 ⑪ : 컨트롤러 인터셉터 적용

OhPro 2022. 10. 31. 13:35
반응형

목차

  • 시작하며
  • 컨트롤러 인터셉터 적용
  • 마치며

 


시작하며

 

이 프로젝트와 관련한 포스팅을 정말 오랜만에 작성하는 것 같다. 사실 이전 포스팅들을 차근차근 읽어보는데, 나름 열심히 작성했지만 알아볼 수 없는 부분도 있었다..... 그래서 이런저런 설명을 구구절절 적는 것보단 간결하게 적기로 다짐했다.

 


 

컨트롤러 인터셉터 적용

 

이전 포스팅에서 이야기했지만, 로그인 유무를 판별하여 서비스를 제공할 것인지 정하는 기능이 필요했다.

 

지금까지는 기능을 만들 때 로그인이 필요한 서비스의 경우, 각각의 컨트롤러 메서드에서 로그인 중인지 체크하는 부분을 일일이 넣었다.

 

이는 심각한 반복 작업이다. 기능을 만들 때 이런 작은 일들이 점차 쌓이는 것은 경험상 파멸로 향하는 길이라는 것을 느낀 적이 있기 때문에 알고 있다.

 

그래서 스프링 인터셉터를 사용하기로 했다.

 

현재는 크게 2 가지의 인터셉터 기능이 필요한데 이는 다음과 같다.

 

  1. 로그인 중이 아닐 때 로그인이 필요한 서비스를 이용하는 경우 이를 막는 인터셉터
  2. 로그인 중인데 로그인 중이 아닐 때 필요한 요청을 하는 경우 이를 막는 인터셉터
    • 예를 들어 로그인 중일 때 회원가입 기능을 사용하는 경우 이에 해당한다.

 

1번의 경우 아래처럼 코드를 작성했다.

 

public class LoginRequireInterceptor implements HandlerInterceptor {

    @Autowired
    LoginManager loginManager;

    public LoginRequireInterceptor(){}

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        Profile profile = (Profile) session.getAttribute(SessionConst.USER_PROFILE);
        if (profile == null){
            response.sendRedirect("/");
            return false;
        }

        if (!loginManager.isCorrectProfile(profile.getAccountNo(), session)) {
            response.sendRedirect("/");
            return false;
        }


        return true;
    }
}

 

코드 리뷰

 

  1. request로부터 session을 획득한다.
  2. 세션에서 profile객체를 획득하고 만약 null이면 첫 페이지로 리다이렉트 하고 실행되려고 했던 컨트롤러가 실행되지 못하게 막는다.
  3. profile객체가 존재했을 경우 loginManager를 통해 올바른 세션인지 검증한다.
  4. 올바르지 않은 profile이었을 경우 첫 페이지로 리다이렉트하고 컨트롤러가 실행되지 못하게 막는다.

 

또한 2번의 경우는 코드를 아래처럼 작성했다.

 

public class LoginNotRequireInterceptor implements HandlerInterceptor {

    @Autowired
    LoginManager loginManager;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        Profile profile = (Profile) session.getAttribute(SessionConst.USER_PROFILE);
        if (profile == null)
            return true;

        if (loginManager.isCorrectProfile(profile.getAccountNo(), session)){
            response.sendRedirect("/main/page");

            return false;
        }

        session.setAttribute("profile", null);
        return true;
    }
}

 

코드 리뷰

 

  1. request로부터 session을 획득한다.
  2. 세션에서 profile객체를 획득하고 만약 profile이 null일 경우 바로 컨트롤러를 실행할 수 있도록 true 값을 리턴한다.
  3. profile객체가 존재했을 경우 loginManager를 통해 올바른 세션인지 검증한다.
    • 만약 올바른 profile객체였다면, 메인 페이지로 리다이렉트하고 컨트롤러가 실행되지 못하도록 막는다.
  4. 올바르지 않은 profile이었을 경우 session에 존재하는 profile을 null로 변경 후 true를 리턴한다.

 

이제 loginManager의 구현 내용을 살펴보자

 

public class LoginManager {

    Map<Integer, HttpSession> loginMap;

    public LoginManager(){
        loginMap = new HashMap<>();
    }

    public boolean addLoginSession(int accountNo, HttpSession session) {
        if (loginMap.containsKey(accountNo))
            return false;
        loginMap.put(accountNo, session);
        return true;
    }

    public void removeLoginSession(int accountNo){
        loginMap.remove(accountNo);
    }

    public void changeLoginSession(int accountNo, HttpSession session){
        loginMap.replace(accountNo, session);
    }

    public boolean isCorrectProfile(int accountNo, HttpSession session){
        if (session == null)
            return false;

        if (!loginMap.get(accountNo).equals(session))
            return false;

        Profile profile = (Profile) session.getAttribute(SessionConst.USER_PROFILE);
        if (profile == null)
            return false;

        if (!loginMap.get(accountNo).getAttribute(SessionConst.USER_PROFILE).equals(profile))
            return false;

        return true;
    }
}

 

코드 리뷰

 

  1. 멤버 변수로 Map<Integer, HttpSession> 타입의 loginMap을 가지고, 해당 클래스의 인스턴스를 생성할 때 new 하도록 작성했다.
    • <Integer : HttpSession>는 <유저의 계정 번호 : 로그인된 세션 정보>의 의미이다.
  2. addLoginSession() : 클라이언트의 로그인 요청 시 loginMap에 해당 유저의 계정 번호와 세션 정보를 등록시키는 메서드
  3. removeLoginSession() : 클라이언트의 로그아웃 요청 시 loginMap에 저장되어있던 유저의 로그인 정보를 지우는 메서드
  4. changeLoginSession() : 클라이언트가 특정 세션으로 로그인 중인 상태에서 유저의 세션이 바뀐 경우에 해당 유저의 로그인 세션을 변경해주는 메서드
    • 만약 loginMap에 저장된 유저의 세션과 현재 요청에서 획득한 유저의 세션이 달랐을 경우 무작정 해당 메서드를 실행하지는 않고, 로그인 세션 변경을 할 것인지 질의하는 페이지에서 "여기서 다시 로그인"을 선택한 경우에만 해당 메서드가 실행되도록 설정했다.
  5. isCorrectProfile() : 계정 번호와 세션을 입력받아 요청에서 받아온 세션이 유효한지(올바른지) 체크하는 메서드로 유효하면 true를 반환한다.

 

로그인 세션 변경 질의 페이지

 

마지막으로 인터셉터를 적용하는 부분의 코드를 보자

 

    @Autowired
    private LoginRequireInterceptor loginRequireInterceptor;

    @Autowired
    private LoginNotRequireInterceptor loginNotRequireInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> loginRequireUrlPatterns = new ArrayList<>();
        loginRequireUrlPatterns.add("/register/page/1");
        loginRequireUrlPatterns.add("/register/page/2");
        loginRequireUrlPatterns.add("/id-usage");
        loginRequireUrlPatterns.add("/account");
        loginRequireUrlPatterns.add("/account/cert");
        loginRequireUrlPatterns.add("/login/page");
        loginRequireUrlPatterns.add("/id/page");
        loginRequireUrlPatterns.add("/id/cert");
        loginRequireUrlPatterns.add("/pw/page/{pageNo}");
        loginRequireUrlPatterns.add("/pw/cert");
        loginRequireUrlPatterns.add("/pw");
        loginRequireUrlPatterns.add("/");

        registry.addInterceptor(loginNotRequireInterceptor)
                .addPathPatterns(loginRequireUrlPatterns);

        registry.addInterceptor(loginRequireInterceptor)
                .excludePathPatterns(loginRequireUrlPatterns)
                .excludePathPatterns("/login");
    }

 

코드 리뷰

 

  1. 해당 메서드는 스프링 프레임워크 자바 설정 클래스이다.
    • WebMvcConfigurer에서 오버라이딩 했다. 
  2. 빈으로 등록된 LoginRequireInterceptorLoginNotRequireInterceptor를 불러오기 위해 @Autowired를 사용하여 불러왔다.
    • 현재 위의 인터셉터가 빈으로 등록되어야 하는 이유는 인터셉터에서 LoginManager객체를 사용해야 하기 때문이다.
    • 인터셉터에서 LoginManager를 사용해야 하는 이유는 로그인 요청 컨트롤러와 같이 특정 컨트롤러 메서드에서 요청을 처리하는 도중에 LoginManager를 사용해야 하는 경우가 생기는데 이때 동일한 LoginManager를 사용하게 하기 위함이다.
  3. url 패턴을 저장하는 리스트를 만들어 LoginNotRequireInterceptor에는 addPathPatterns()로 설정했고, LoginRequireInterceptor에는 excludePathPatterns()로 설정했다. 
    1. 일반적으로 로그인이 필요한 서비스와 그렇지 않은 서비스는 공통이 될 수가 없기 때문에 LoginRequireInterceptor에 설정된 uri가 LoginNotRequireInterceptor에도 존재할 수는 없기 때문이다.
  4. 로그인과 로그아웃 요청 관련 uri를 동일하게 "/login"로 설정하고 메서드를 나눴기 때문에 로그인과 로그아웃 요청이 동작하지 않는 문제가 있어서 "/login"의 uri는 인터셉터에 포함시키지 않았다.

 

이렇게 인터셉터를 저장하고 실행했을 때 정상적으로 로그인이 필요한 서비스엔 로그인 상태여야 접근이 가능하고, 로그인이 필요하지 않은 서비스엔 로그인이 되지 않은 상태여야만 접근이 가능하게 되었다!

 


 

마치며

뭔가 기능 구현이나 수정은 종종 일어나는데 블로그에 포스팅하는 것을 잊지 않는 것이 어려운 것 같다. 요즘은 알고리즘 문제도 종종 풀면서 뭔가 기한 없는 프로젝트를 진행하는 기분이다. (사실 맞는 말...)

 

그리고 사실 세션에 유효기간 기능을 추가해야 하는데, 세션 프로퍼티에 추가하고, loginManager의 isCorrectProfile() 메서드 내부에서 시간 체크를 하도록 하면 어떨까 하는 생각이다. (방금 떠올랐는데 괜찮은 것 같기도....)

 

감사합니다!!

반응형