블로그 토이프로젝트 진행 중에 인증과 인가 부분을 인터셉터로 구현해서 진행하였다가 해결해야할 문제가 발견됐다.
글을 작성하는 URL은 POST /posts
글을 조회하는 URL은 GET /posts/글번호
글을 수정하는 URL은 PUT /posts/글번호
글을 삭제하는 URL은 DELETE /posts/글번호
여기서 로그인한 사람(USER)은 POST, GET, PUT, DELETE 요청을 모두 할 수 있다.
로그인하지 않은 사람(GUEST)은 GET을 요청할 수 있다.
문제는 여기서 발생한다.
요청 URL을 인가인터셉터를 태우기 위해서는 인가인터셉터.addPathPatterns()에 요청 URL을 등록해주어야한다.
글 작성, 수정, 삭제는 인가인터셉터를 통과해야하고, 따라서 인가인터셉터.addPathPatterns("/posts/**")와 같이 등록해야한다.
그러면 /posts/**에 해당하는 요청은 인가인터셉터의 로직에 따라 USER는 허용하고, GUEST는 막는다.
@Configuration(value = "webConfig")
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor())
.addPathPatterns("/posts/**");
}
}
public class AuthorizationInterceptor implements HandlerInterceptor {
private final Authorization authorization;
public Authorization(Authorization authorization) {
this.authorization = authorization;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return authorization.authorize(request, response, handler); //USER면 true, GUEST면 false반환
}
}
문제는 글 조회에 해당하는 GET /posts/글번호 또한 인가인터셉터의 필터링 대상이 되어서 USER는 허용하고 GUEST는 막게 되는 것이다.
이 문제를 해결하기 위해서는, GET메서드일 경우에는 인가인터셉터의 로직을 적용하는 것이 아닌, GUEST와 USER 모두 통과시켜주어야 할 것 같았다.
처음으로 생각한 해결책이다.
1. AuthorizationInterceptor.prehandle()메서드 내부에 if문을 넣어서 GET메서드 요청이면 return true를 해준다.
간단한 방법이지만 관리가 쉽지 않을 것 같았다.
1. 관리 포인트가 2곳으로 늘어난다.
인가인터셉터의 접근 URL은 WebConfig에서 하지만 접근 제어하는 Method는 인가인터셉터에서 한다는 점이다.
2. 관심사가 다르다.
인가인터셉터의 목적은 들어온 요청에 대해 USER면 true, GUEST면 false를 반환하는 역할을 맡고 있었는데, 메서드에 따라서 통과 여부를 가려야하는 로직이 추가된다.
3. 깔끔하지가 않다. (if문이 계속 추가된다.)
만약 요구사항이 바뀌어 글 생성을 GUEST도 할 수 있도록 하게 된다면, 인가인터셉터의 prehandle() 내부에 if문을 넣어서 POST메서드 요청이면 return true를 해주어야한다.
따라서 다른 방법을 생각해보았고, 해당 역할을 다른 객체에게 맡겨보자는 생각이 들었다. 인가인터셉터를 한 번 감싸는 객체를 만들고, 그 객체에게 URL과 Method를 보고 인가인터셉터에게 요청을 보낼지 말지 결정하는 역할을 주기로 했다.
public class MvcMatcherInterceptor implements HandlerInterceptor {
private final HandlerInterceptor handlerInterceptor;
private final List<MvcPath> excludePaths;
public MvcMatcherInterceptor(HandlerInterceptor handlerInterceptor) {
this.handlerInterceptor = handlerInterceptor;//인가인터셉터를 주입한다.
}
public HandlerInterceptor addExcludePattern(HttpMethod method, String pattern) {
excludePaths.add(new MvcPath(method, pattern));
return this;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(excludePaths.stream()
.anyMatch(excludePath -> excludePath.matches(request.getMethod(), request.getServletPath())) {
return true;
}
return handlerInterceptor.preHandle(request, response, handler);
}
static class MvcPath {
private final HttpMethod method;
private final String pattern;
public MvcPath(HttpMethod method, String pattern) {
this.method = method;
this.pattern = pattern;
}
public boolean matches(HttpMethod method, String path) {
return false;//this.method, this.pattern 과 method, path비교
}
}
}
@Configuration(value = "webConfig")
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor())
.addPathPatterns("/posts/**");
}
private HandlerInterceptor authorizationInterceptor() {
return new MvcMatcherInterceptor(new AuthorizationInterceptor())
.addExcludePattern(HttpMethod.GET, "/post/**");
}
}
이렇게 해주면 URL과 Method를 보고 인가인터셉터에 접근하게 할지 말지 결정하는 역할은 MvcMatcherInterceptor가 맡게 된고, 인가인터셉터는 원래 자신이 하던 일만 잘 하면 된다.
ps.
이렇게 원래 객체에게 접근을 할지 말지 제어할 수 있는 객체를 사용하는 패턴을 프록시패턴이라고 한다고 한다.
vs 데코레이터패턴.
데코레이터패턴: 원래 객체로의 접근을 제어하지 않음.
프록시패턴: 원래 객체로의 접근을 제어함.