로그인 안한 회원이 상품 등록페이지에 들어가면 안된다.
이렇게 어플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사(cross-cutting, concern)라고 한다. 여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다.
이러한 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 인터셉터를 사용하는 것이 좋다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest
를 제공한다.
필터는 서블릿이 지원하는 수문장이다. 필터의 특성은 다음과 같다.
필터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터를 적용하면 필터가 호출된 다음에 서블릿이 호출된다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다.
또, 필터는 특정 URL 패턴에 적용할 수 있다. /*
라고 하면 모든 요청에 필터가 적용된다.
필터 제한
HTTP요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) // 비 로그인 사용자
필터 체인
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
필터는 체인으로 구성되고, 중간에 필터를 자유롭게 추가할 수 있다.
로그 다음에 로그인 여부 체크할 수도 있다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
init()
: 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
doFilter()
: 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
destroy()
: 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
Filter 인터페이스를 구현한다.
doFilter
doFilter
가 호출된다.ServletRequest request
는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP를 사용하려면 HttpServletRequest
로 다운 케스팅 하면 된다.UUID
uuid
를 생성해둔다.chain.doFilter(request, response)
스프링부트를 사용한다면 FilterRegistrationBean
을 사용해서 등록하면 된다.
setFilter(new LogFilter())
: 등록할 필터를 지정한다.
setOrder(1)
: 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
addUrlPatterns("/*")
: 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
실무에서 HTTP 요청과 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자.
인증체크 필터를 만들어보자~
먼저 화이트 리스트를 만든다. 이 화이트리스트는 로그인을 하지 않아도 이동할 수 있도록 필터에서 제외시켜주는 기능을 한다.
spring의 PatternMatchUtils를 사용해서 화이트 리스트에 없으면 true를 반환하고 화이트 리스트 목록들을 false를 반환한다.
response
와 request
둘다 Http 서블릿으로 바꿔주고, 로그인 했는지 검증이 필요한 부분에서 세션을 가져와 로그인을 하지 않은 상태라면 sendRedirect()
를 이용해 로그인 페이지로 보내준다.
/login?redirectURL=
은 로그인 후에 이전에 접속을 시도했던 곳으로 보내주기 위해 URI를 넣어주는 것이다.
그 후에 다음 체인으로 넘어간다.
여기를 보면 1 -> 2 -> 1 순으로 내려왔다.
1에서 chain.doFilter()로 2 로 넘어가고 2가 끝나니 1에서 이후 로직이 실행되었기 때문이다.
그런데 실제 실행을 해보면 로그인 이후에 이전에 접속하려 했던 페이지로 넘어가지 않고 홈으로 간다.
로그인 컨트롤러에서 마지막에 그냥 리다이렉트로 홈으로 보내기 때문이다. 그러므로 로그인 컨트롤러를 약간 변경해준다.
@RequestParam
redirectURL을 받아준다. 못받을 경우를 생각해서 기본값을 /
로 설정한다.
그 값을 마지막 리턴값에 넣어준다.
참고
필터에는 다음에 설명할 스프링 인터셉터는 제공하지 않는, 아주 강력한 기능이 있는데 chain.doFilter(request, response);
를 호출해서 다음 필터 또는 서블릿을 호출할 때 request
, response
를 다른 객체로 바꿀 수 있다. ServletRequest
, ServletResponse
를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용된다. 잘 사용하는 기능은 아니다.
스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.
필터는 서블릿이, 인터셉터는 스프링MVC가 제공하는 기술이다.
스프링 인터셉터 흐름
HTTP요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터 제한
HTTP요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청, 컨트롤러 호출X) // 비 로그인 사용자
스프링 인터셉터 체인
HTTP요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
preHandle
: 컨트롤러 호출 전에 호출(응답값이 true
면 다음으로 진행, false
면 더 이상 진행X
postHandle
: 컨트롤러 호출 후에 호출된다.
afterCompletion
: 뷰가 렌더링 된 이후에 호출.
예외 상황
preHandle
: 컨트롤러 호출전에 호출
postHandle
: 컨트롤러에서 예외가 발생하면 호출X
afterCompletion
: 항상 호출. 이 경우 예외(ex
)를 파라미터로 받아서 예외가 담아짐.
필터를 꼭 사용해야 하는 경우가 아니면 인터셉터 사용하는 것이 훨씬 편리함.
String uuid = UUID.randomUUID().toString()
uuid
생성request.setAttribute(LOG_ID, uuid)
preHandle
의 값을 afterCompletion
에서 사용하려면 값을 어딘가에 담아둬야하고, 값을 request
에 담고 aftretCompletion
에서 꺼내 사용한다.
return true
true
면 정상 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.HandlerMethod
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 스프링을 사용하면 일반적으로 @Controller
, @RequestMapping
을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod
가 넘어온다.
ResourceHttpRequestHandler
@Controller
가 아니라 /resource/static
와 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandelr
가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요하다.
WebMvcConfigurer
가 제공하는 addInterceptors()
를 사용해서 인터셉터를 등록할 수 있다.
registry.addInterceptor(new LogInterceptor())
: 인터셉터를 등록한다.
order(1)
: 인터셉터의 호출 순서를 지정한다. 낮을수록 먼저 호출된다.
addPathPatterns("/**")
: 인터셉터를 적용할 URL 패턴을 지정한다.
excludePathPatterns("/css/**", "/*.ico", "/error")
: 인터셉터에서 제외할 패턴을 지정한다.
이렇게 인터셉터는 addPathPatterns
, excludePathPatterns
로 매우 정밀하게 URL 패턴을 지정할 수 있다.
스프링의 URL 경로는 더 자세하고 세밀하게 설정할 수 있다.
PathPattern 공식문서
? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring" {spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
/pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/
toast.html
/resources/*.png — matches all .png files in the resources directory
/resources/** — matches all files underneath the /resources/ path, including /
resources/image.png and /resources/css/spring.css
/resources/{*path} — matches all files underneath the /resources/ path and
captures their relative path in a variable named "path"; /resources/image.png
will match with "path" → "/image.png", and /resources/css/spring.css will match
with "path" → "/css/spring.css"
/resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the
value "spring" to the filename variable
인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns
와 excludePathPatterns
에 작성하면 된다. 기본적으로 모든 경로에 해당 인터셉터를 적용하되 (/**
), 홈(/
), 회원가입(/members/add
), 로그인(/login
), 리소스 조회(/css/**
), 오류(/error
)와 같은 부분은 로그인 체크 인터셉터를 적용하지 않는다.
서블릿 필터에 비해 매우 편리함.
@Target(ElementType.PARAMETER)
: 파라미터에만 사용
@Retention(RetentionPolicy.RUNTIME)
: 리플렉션 등을 활용할 수 있도록 런타임까지 어노테이션 정보가 남아있음
supportsParameter()
: @Login
어노테이션이 있으면서 Member
타입이면 해당 ArgumentResolver
가 사용된다.
resolveArgument()
: 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. 여기서는 세션에 있는 로그인 회원 정보인 member
객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member
객체를 파라미터에 전달해준다.