Spring의 HandlerInterceptor 구현을 통해 사용자 요청을 가로채어 처리할 수 있는 인터셉터입니다.
이 인터셉터는 특정 핸들러 메서드에 커스텀 어노테이션이 붙어 있을 경우, 해당 요청이 유효한지 검증합니다.
저는 HandlerInterceptor를 통해 인증된 사용자의 권한과 특정 조건을 검사하기 위해 사용했습니다.
로그인한 사용자가 게시글의 작성자인지 확인이 필요한 경우
서비스를 사용하다 보면 로그인 없이 접근 가능한 페이지(홈 페이지 등)가 있고, 로그인을 하거나 추가 권한이 있는 경우에만 접근이 가능한 페이지(게시글 수정, 삭제, 마이페이지)가 존재합니다.
1. 커스텀 어노테이션 만들기
/**
* @CheckCombinationOwner : 현재 사용자가 게시글을 작성한 사용자인지 확인합니다.
* @Retention : 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정합니다.
* @Target : 어노테이션이 사용될 위치를 지정합니다.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckCombinationOwner {
}
2. HandlerInterceptor 인터페이스 구현하기
@Component
@RequiredArgsConstructor
@Slf4j
public class CheckCombinationOwnerInterceptor implements HandlerInterceptor {
private final JwtProvider jwtProvider;
private final CombinationQueryService combinationQueryService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// Handler가 메소드인 경우에만 진행
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 핸들러 메서드에 @CheckCombinationOwner 어노테이션이 있는지 확인
if (handlerMethod.getMethod().isAnnotationPresent(CheckCombinationOwner.class)) {
// JWT를 통해 로그인 사용자 정보 조회
Member loginMember = getLoginMember(request);
// URL에서 combinationId 추출
Long combinationId = extractCombinationId(request);
// 로그인한 사용자가 작성자인지 확인
if (loginMember != null) {
Boolean isOwner = combinationQueryService.isCombinationOwner(combinationId,
loginMember);
if (!isOwner) {
throw new ApiException(ErrorStatus._FORBIDDEN_MEMBER_REQUEST);
}
} else {
throw new ApiException(ErrorStatus._UNAUTHORIZED);
}
}
}
return true;
}
}
- HandlerMethod : 실행될 핸들러(컨트롤러의 메서드) CheckCombinationOwner가 null이라면 로그인 없이 접근가능한 핸들러이므로 true를 return 합니다.
Interceptor의 실행 메서드는 크게 preHandler() , postHandler() , afterCompletion() 로 구성되어 있습니다.
- preHandler() : 컨트롤러 메서드(핸들러)가 실행되기 전에 작동합니다.
- postHandler() : 컨트롤러 메서드(핸들러) 실행 직 후, view 페이지가 렌더링 되기 전에 작동합니다.
- afterCompletion() : view 페이지가 렌더링 되고 난 후 작동합니다.
HanlderInterceptor를 통해 컨트롤러의 메서드 실행 직후에 해당 요청을 가로채서 처리하는 과정은 아래와 같습니다.
HandlerMethod : 실행될 컨트롤러의 메서드 (핸들러)
CheckCombinationOwner: 해당 핸들러의 이전에 만든 어노테이션이 존재하는지 확인합니다.
request Header에 JWT 토큰을 파싱 하여 사용자 정보를 가져와 데이터베이스에 저장된 사용자 정보를 가져옵니다.
request에 URL에 작성된 속성을 파싱하여 게시글 Id( combinationId)를 조회합니다.
private Member getLoginMember(HttpServletRequest request) {
String token = jwtProvider.getJwtTokenFromHeader(request);
return jwtProvider.getMemberFromToken(token.split(" ")[1]);
}
private Long extractCombinationId(HttpServletRequest request) {
return Optional.ofNullable(
request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE))
.map(Map.class::cast)
.map(map -> map.get("combinationId"))
.map(Object::toString)
.map(Long::parseLong)
.orElseThrow(() -> new ApiException(ErrorStatus._BAD_REQUEST));
}
로그인한 사용자가 있는 경우와 없는 경우,
로그인한 사용자가 게시글을 작성했는지, 안 했는지를 검증하여 예외 처리를 합니다.
해당 @CheckCombinationOwner가 메서드에 작성되지 않거나, 로그인한 사용자가 해당 게시글을 작성한 사용자인 경우에는 true를 반환하여 해당 메서드를 계속 수행합니다.
3. Webconfig에 Interceptor 등록하기
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final CheckCombinationOwnerInterceptor checkCombinationOwnerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkCombinationOwnerInterceptor);
}
}
이렇게 함으로써, 요청이 컨트롤러로 전달되기 전에 로그인한 사용자의 권한을 확인하여 게시글의 작성자인지를 판단하고, 작성자가 아닌 경우에는 요청을 거부할 수 있습니다.
이는 보안상 중요한 기능으로, 로그인 사용자가 자신의 데이터에만 접근할 수 있도록 보장합니다.