주니어 기초 코딩공부/Spring 기초

[Spring] 스프링 인터셉터(Interceptor) 정리_feat. 인터셉터를 이용한 로그인 구현

jju_developer 2023. 2. 10. 01:05
728x90

안녕하세요 jju_developer입니다.

오늘은 드디어 스프링 인터셉터를 이용한 로그인 구현하는 로직에 대해 배웠습니다!

우선 인터셉터가 뭐길래 로그인할 때 필요한 것일까요?

1. Spring MVC의 인터셉터(Interceptor)?

컨트롤러(Controller)의 '핸들러(Handler)'를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 일종의 필터

intercepter 란 단어는 '낚아채다'라는 의미입니다.

 

인터셉터는 사용자 요청에 의해 서버에 들어온 Request 객체를

컨트롤러의 핸들러(사용자가 요청한 url에 따라 실행되어야 할 메서드, 이하 핸들러)로

도달하기 전에

 

결론적으로 내가 원하는 url로 가기 전에 잠깐! 너 로그인되어있어? 잠깐! 너 세션이나 쿠키에 정보 있어?라고 

확인하는 단계를 거치고 나서 로그인 정보(세션 또는 쿠키)가 있으면 해당 url로 보내주고 아니면 다시 로그인 화면으로 돌려주는 것입니다.

 

1.1 Filter와 인터셉터의 공통점과 차이점

jsp에서 필터를 사용했었는데 그럼 필터와 인터셉터의 다른 점은 무엇일까요????

▼▼▼▼필터를 이용한 로그인 ▼▼▼▼

 

[JSP] 필터란 무엇인가

안녕하세요 jju_developer입니다. 오늘은 JSP의 필터의 기능은 무엇이고 어떻게 사용하는지에 대해 알아보도록 하겠습니다. 필터는 말 그대로 클라이언트의 요청을 선처리하거나 서버의 자원을 가

jju240.tistory.com

 

Servlet 기술의 Filter와 Spring MVC의 HandlerInterceptor는 특정 URI에 접근할 때 제어하는 용도로 사용된다는 공통점을 가지고 있습니다.


단!!  Filter의 경우 웹 애플리케이션 내에서 동작하므로, 스프링의 Context를 접근하기 어렵습니다.

 

Interceptor의 경우 스프링에서 관리되기 때문에

스프링 내의 모든 객체(빈)에 접근이 가능하여 활용할 수 있습니다.

 

HandlerInterceptor는 Filter와 유사하게 HttpServletRequest, HttpServletResponse를 파라미터로 받는 구조입니다.

 

(1) HandlerInterceptorAdapter 클래스
HandlerInterceptor는 인터페이스로 정의되어 있지만, HandlerInterceptorAdaptor는 HandlerInterceptor를 쉽게 사용하기 위해서 인터페이스의 메서드를 미리 구현한 클래스입니다.

 

 

1.2 왜 인터셉터를 사용하는가?

개발자는 특정 Controller의 핸들러가 실행되기 전이나 후에 추가적인 작업을 원할 때 Interceptor를 사용합니다.

(추가적인 작업으로는 로그인체크, 권한 체크 등이 있습니다.)

권한 체크 예를 통해서 개발자가 인터셉터의 어떠한 이점 때문에 사용하기를 원하는지 살펴보면,

개발자가 관리자 계정만이 실행할 수 있는 Controller 핸들러를 작성한다고 가정한다면

개발자는 오직 관리자 계정만 실행할수 있도록 하기 위해 핸들러에 접근하는 사용자가 관리자 인지 확인하는

세션 체크 코드를 각 핸들러에 작성해줘 하겠죠?

 

작성해주어야 할 핸들러수가 적다면 문제가 되지 않겠지만, 적용해야 할 핸들러가 수천 개가 된다면 어떻게 될까요?

 

크게 두 가지 문제가 생깁니다.

1) 메모리 낭비, 서버의 부하가 늘어난다. 

적용해야 할 핸들러만 수만큼 세션체크 코드를 작성함으로써 반복되는 코드들이 매우 많아지기 때문입니다.

2) 코드의 누락

사람이 작성을 하는 것이기 때문에 누락과 같은 실수가 발생할 수밖에 없습니다.

 

 만약 회원정보에 접근하는 핸들러가 세션 체크가 누락되어서 관리자 인지 확인을 안 한다면,

자격이 없는 사용자가 접근할 수 있게 되어 보안적으로 큰 문제를 가지게 됩니다.

 

이러한 문제점을 줄이기 위해서, 개발자는 인터셉터를 사용할 수 있습니다.

 

인터셉터를 사용하게 되면 개발자는 핸들러 수만큼 작성했던 세션 체크 코드를 인터셉터 클래스에 한 번만 작성하면 됩니다.

 

이렇게 하게 되면 코드도 현저히 줄기 때문에 메모리 낭비를 줄어들겠죠?

 

그리고 인터셉터 적용의 유무 기준이 되는 url을 servlet-context.xml에 설정해 주게 되면 스프링에서 일괄적으로

해당 url 경로의 핸들러에 인터셉터를 적용해 주기 때문에 누락에 대한 위험이 상당히 줄게 됩니다.

 

 

1.3 인터셉터의 메서드

스프링이 제공해 주는HandlerInterceptor 인터페이스와 HandlerInterceptorAdapter 추상클래스에 정의되어 있는

메서드는 preHandle(), postHandle(), afterCompletion() 3가지입니다.

 

1) preHandle()

  • 컨트롤러가 호출되기 전에 실행
  • 컨트롤러가 실행 이전에 처리해야 할 작업이 있는 경우 혹은 요청정보를 가공하거나 추가하는 경우 사용
  • 실행되어야 할 '핸들러'에 대한 정보를 인자값으로 받기 때문에 '서블릿 필터'에 비해 세밀하게 로직을 구성할 수 있음
  • 리턴값이 boolean이다. 리턴이 true 일경우 preHandle() 실행 후 핸들러에 접근한다. false일 경우 작업을 중단하기 때문에 컨트롤러와 남은 인터셉터가 실행되지 않습니다.

2) postHandle()

  • 핸들러가 실행은 완료되었지만 아직 View가 생성되기 이전에 호출됩니다.
  • ModelAndView 타입의 정보가 인자값으로 받습니다.
    따라서 Controller에서 View 정보를 전달하기 위해 작업한 Model 객체의 정보를 참조하거나 조작할 수 있습니다.
  • preHandle()에서 리턴값이 fasle인 경우 실행되지 않습니다.
  • 적용 중인 인터셉터가 여러 개 인경우, preHandle()는 역순으로 호출됩니다.
  • 비동기적 요청처리 시에는 처리되지 않습니다.

 

3)  afterCompletion()

  • 모든 View에서 최종 결과를 생성하는 일을 포함한 모든 작업이 완료된 후에 실행됩니다.
  • 요청 처리 중에 사용한 리소스를 반환해 주기 적당한 메서드이다.
  • preHandle()에서 리턴값이 false인 경우 실행되지 않습니다.
  • 적용 중인 인터셉터가 여러 개인 경우preHandle()는 역순으로 호출됩니다.
  • 비동기적 요청 처리 시에 호출되지 않습니다.

 

2. Interceptor 동작 위치 및 순서 정리

1) 사용자는 서버에 자신이 원하는 작업을 요청하기 위해 url을 통해 Request 객체를 보냅니다.

2) DispatcherServlet은 해당 Request 객체를 받아서 분석한 뒤

'핸들러 매핑(HandlerMapping)' 에게 사용자의 요청을 처리할 핸들러를 찾도록 요청합니다.

3) 그 결과로 핸들러 실행체인(HandlerExectuonChanin)이 동작하게 되는데,

이 핸들러 실행체인은 하나이상의 핸들러 인터셉터를 거쳐서 컨트롤러가 실행될 수 있도록 구성되어 있습니다.

(핸들러 인터셉터를 등록하지 않았다면, 곧바로 컨트롤러가 실행되며,

반대로 하나 이상의 인터셉터가 지정되어 있다면 지정된 순서에 따라서 인터셉터를 거쳐서 컨트롤러를 실행합니다)

 

 

3. 인터셉터 예제 - RememberMe

로그인을 하는 부분에서

아이디:        user00
  비번 :        user00
useCookie:  true

로그인창

로그인을 하게 되면 위와 같은 사진이 됩니다.

 

순차적으로 설명을 하자면,

 

1. 로그인을 하기 위해 브라우저에 /user/login으로 request를 보냅니다.

 

UserController에서 loginGet()이라는 메서드가 작동하게 됩니다.

 

mySQL에 있는 tbl_user 테이블에 있는 칼럼과 동일한 vo객체를 만들었습니다.

uservo 객체를 볼까요?

select* from tbl_user;
UserVO

vo는 자바 객체를 담는 용도입니다.

 

UserDAO의 생성 및 SQL 처리는 

// org.zerock.persistence.UserDAO
package org.zerock.persistence;
import java.util.Date;
import org.zerock.domain.UserVO;
import org.zerock.dto.LoginDTO;
public interface UserDAO {
public UserVO login(LoginDTO dto)throws Exception;
public void keepLogin(String uid, String sessionId, Date next);
public UserVO checkUserWithSessionKey(String value);
}

클래스가 아닌 인터페이스로 UserDao를 만들고 구현하는 구현 클래스를 만들었습니다.

이 뜻은 구현한 클래스를 여러 개 만들며 이름을 통일하게 하는 목적이 있습니다.

UserDAOImpl implements UserDAO

 

서블릿 컨텍스트.xml에 정의된 인터셉터를 볼까요?

아까 위에서 설명한 인터셉터 적용의 유무 기준이 되는 url을 servlet-context.xml에 설정해 주게 되면 스프링에서 일괄적으로

해당 url 경로의 핸들러에 인터셉터를 적용해 주는 내용이 바로 이 부분입니다.

 

아래 /sboard/ 4개의 페이지에는 로그인이 필요한 페이지기 때문에

url에 해당 페이지를 request 요청하게 될 경우에는 authInterceptor가 실행됩니다.

 

authInterceptor에는 preHandle()과 postHandle()이 있죠?

pre에서 세션에 로그인이 있냐고 물어보는 것입니다.

로그인을 할 때 세션에 로그인 객체를 담았는데

(만약 로그인이 안되어있다면, 메시지 출력과 함께 로그인 화면으로 돌려보내줍니다!!!)


우리가 로그인을 하고 브라우저를 죽이고(창을 다 끄고) 다시 웹을 열어서 /sboard 링크로 들어가게 된다면

세션이 삭제되었기 때문에 다시 로그인 창으로 가야 합니다. 

 

그런데 만약에

여기에 Remember Me를 체크하게 된다면..!!!

 

1. login.jsp에 정의된 <Input type="checkBox" name="userCookie"> rememberMe

이렇게 정의된 userCookies가 true가 됩니다.

(왜냐 저 리멤버미 체크 박스의 이름은 userCookie이기 때문이죠)

 

2. 그다음 로그인 인터셉터로 가서 pre가 실행되어 원래 있던 세션이 삭제되고 새로운 세션을 집어넣습니다.

 

 

 

3. use 컨트롤러에서 로그인 DTO 객체를 생성하고

 

4. model.attribute("userVO",vo) 모델의 속성에 userVO 객체를 집어넣습니다.

5. 다시 로그인 인터셉터에서 post가 실행됩니다.

모델의 userVO 객체를 가져오고 세션에 넣은 뒤,

if(request.getParameter("useCookie"!=null) 만약 쿠키를 가져왔을 때 값이 없지 않다면

로그인 쿠키라는 것을 만들고 거기에 세션 ID를 넣으라는 뜻입니다!!

즉, 세션이 있다면 그 세션 아이디를 로그인 쿠키에도 담는 것이죠~

loginUnterceptor.java

if 만약에 쿠키가 파라미터로 들어와서 널값이 아닐 때 세션의 아이디를 가져와서 로그인 쿠키라는 곳에 값을 저장합니다.

그리고 그 로그인 쿠키의 setPath를 세팅해 주었는데 이것은 루트라는 패스에 다 적용을 하겠다는 뜻이며,

setMaxAge는 이 로그인쿠키값을 일주일 동안! 저장하겠다는 뜻입니다.

그리고 최종적으로 response.addCookie로 브라우저로 쿠키의 값을 전달하게 됩니다

🍪🍪🍪🍪🍪🍪🍪🍪

 

6. response.addCookie("loginCookie") 리스폰스를 통해서 로그인 쿠키를 브라우저에 전달하게 됩니다.

 

그다음 auth인터셉터를 보겠습니다.

 

7. 자, 어스인터셉터에서 세션이 없는 경우에 dest에 내가 가고자 하는 주소를 담습니다.

saveDest(request)를 통해서 원래 내가 가고자 하는 주소를 담았습니다.

해당 주소는 추후에 로그인을 다시 했을 때 그냥 일반 페이지로 가는 것이 아니라,

아까 전에 내가 가고 싶었으나 로그인이 안되어있어서 못 간 그 페이지로 보내주는 주소를 저장한 것입니다.

 

여기서 세션만 확인하는 것이 아니라, 쿠키도 확인하게 되는 것이죠!

아까 브라우저를 죽이게 되면 세션도 같이 죽게 되니까 remember Me를 한 의미가 없어지죠?

그래서 여기서 쿠키가 null 이 아니면 아까 만든 쿠키를 가져와서 서비스의 checkLoginBefore에 

쿠키를 넘겨주는 것입니다.

 

8. UserServiceImpl는 UserService를 구현한 구현 클래스입니다.

 

여기서 마지막 override 부분을 보면

checkLoginBefore에서 얻어온 쿠키의 벨류값을 dao로 넘겨주고 있죠?

 

9. UserDAOImpI.java 로 가볼까요?

여기서 나오는 checkUserWithSessionKey를 데이터 베이스에 검색해 보겠습니다.

 

세션키가 데이터베이스에 이미 들어가 있는 것을 볼 수 있습니다!!

이렇게 아까 rememberMe 박스를 누르게 되면 로그인 쿠키에 저장이 되죠?

로그인하는 많은 아이디 중에서 체크박스를 누른

그렇게 저장한 아이디만 세션키에 세션 ID가 동일하게 이름이 로그인 쿠키인 곳에 저장이 되는 것입니다!!

 

UserDAO에서 이제 keepLogin을 보면 맵에 저장된 값을 불러와 매칭시킵니다.

 

10. UserController

이렇게 저장된 세션이 하나 선택되어 dto에 정보가 저장이 되었고,

User컨트롤러에서 포스트 방식으로 들어온 값이 실행이 되는데,

코드를 보면

if(dto.isUseCookie())이 부분이 true로 되겠죠? 쿠키가 있으니까

그러면 일주일 동안 세션 리밋을 정하고 KeepLogin을 하게 서비스로 넘기는 것입니다...!!!

 

11. UserMapper.xml

UserMapper.xml을 확인해 보면

KeepLogin이 보이시죠?

저기서 이제 DB를 업데이트 하느느 것 입니다!!!

 

맵객체에서 가져온 정보들을 다 업데이트를 하는 것이죠

 

 

여기 DB로 검색해 보면 세션 만료일이 있습니다.

 

이렇게 같은 아이디일지라도 로그인을 다시 할 때마다 sessionKey의 이름이 달라지게 됩니다.

 

자! 이렇게 오늘 배운 인터셉터를 이용한 로그인 구현 방법을 정리해 보았는데요!

 

혹시라도 부족하거나 이해가 잘 안 가는 부분은 댓글로 알려주시면

감사하겠습니다.

 

아직 많이 어렵지만 반복해서 보다 보면 늘어가겠죠..?

 

그럼 오늘도 수고하셨습니다

 

감사합니다 🥳

728x90