순간을 성실히, 화려함보단 꾸준함을

Http method 에 따라 동일한 URI 를 다르게 처리하는 방법 본문

나의 개발 메모장

Http method 에 따라 동일한 URI 를 다르게 처리하는 방법

폭발토끼 2023. 6. 3. 14:12

안녕하세요!

벌써 글또 8번째 글을 작성해야 하는 기한이 다가왔습니다.

 

오늘은 토이 프로젝트를 진행하다 마주했던 상황을 풀어 써보려고 합니다.

 

배경

로그인을 성공한 뒤에 인증된 사용자인지 아닌지를 구별하기 위해서 spring interceptor 에서 체크하는 로직을 생성하였습니다. 이때, 동일한 URI 에 대해서 http method 에 따라 다르게 로직을 수행해야 하는 상황을 맞이하였습니다.

예를 들면,

 

Get : /api/member/{id}/me(회원정보 조회)

Patch : /api/member/{id}/me(회원정보 수정)

 

위와 같이 http method 가 정의된 API가 존재하고 있을때, 회원정보를 조회하는 API는 누구나 접근가능하지만 회원정보를 수정하는 API는 반드시 본인만 수행해야 되겠죠?

 

이런 상황일때 interceptor 에서 어떻게 호출한 API의 정보를 추출하여 구분해줄 수 있을지 고민이 들었었습니다.

 

분석

열심히 서치한 결과 spring interceptor 에 prehandler 라는 메소드는 3번째 인자로 handler 라는 인자를 받습니다.

그리고 이 handler 는 HandlerMethod 로 치환이 가능했고 HandlerMethod 는 호출한 메서드에 대한 메타데이터들을 가지고 있던 것 이었습니다.

HandlerMethod 란?
메서드 매개변수, 메서드 반환 값, 메서드 주석 등에 대한
메타데이터를 편리하게 접근할 수 있는 기능을 제공하는 클래스

그럼 이 HandlerMethod 를 어떻게 이용할 수 있을까요???

내가 원하는 메소드에만 권한 체크 없이 요청을 처리하고 싶을텐데 말이죠

 

바로 Custom Annotation 을 활용하여 이 문제를 해결 할 수 있습니다.

 

즉, 임의의 어노테이션을 생성한 후 권한체크를 하고 싶지 않은 메소드에 붙여줍니다. 그러면 interceptor 에서 요청한 method 에 대해 어떤 어노테이션이 붙여져 있는지 체크할 수 있고 해당 어노테이션이 부착되어 있다면 권한체크 없이 패스해 줄 수 있겠죠.

 

실행과정

먼저 Custom Annotation 을 생성해 주도록 합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}

NoAuth 라는 어노테이션은 메소드에 붙일 용도이니 target 을 method 로 retention 은 기본적으로 사용되는 Runtime 으로 설정해 주었습니다.

 

※ Annotation 의 Retention 을 Runtime 으로 설정하면 어떤 차이점이 있는걸까요?????

바로 Java 에서 제공해주는 Reflection 이 가능하다는 점 입니다. Reflection 이 무엇인지 모르겠으면 Java 기본책이나 블로그를 통해서 학습해보시길 추천드립니다!!(저도 아직 reflection 을 활용해서 실무적으로 적용한 적은 한번도 없습니다)

java.lang.reflect (Java SE 11 & JDK 11 ) (oracle.com)

 

그리고 권한체크를 하지 않을 method 에 이 어노테이션을 부착해주겠습니다.

    /**
     * 회원가입
     */
    @NoAuth
    @ResponseStatus(HttpStatus.OK)
    @PostMapping
    public EmptyJsonDTO signUp(@RequestBody MemberSignUpRequestDTO signUpRequestDTO){
        log.info("signUpRequestDTO = {}",signUpRequestDTO.toString());
        memberService.joinMember(signUpRequestDTO);
        return new EmptyJsonDTO();
    }

(회원가입은 누구나 요청할 수 있는 서비스이니 @NoAuth 라는 어노테이션을 붙여주었습니다)

 

그리고 interceptor 에 요청 method 에 @NoAuth 라는 어노테이션이 붙여져 있는지 확인하여 처리하는 로직을 추가해줍니다.

package share_diary.diray.config;

import lombok.RequiredArgsConstructor;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.WebUtils;
import share_diary.diray.auth.AuthService;
import share_diary.diray.auth.domain.NoAuth;
import share_diary.diray.exception.http.UnAuthorizedException;
import share_diary.diray.exception.jwt.AccessTokenRenewException;
import share_diary.diray.exception.jwt.TokenIsNotValidException;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private final AuthService authService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if(handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            NoAuth noAuth = handlerMethod.getMethodAnnotation(NoAuth.class);
            if(noAuth != null){
                return true;
            }
        }

        String accessToken = request.getHeader("Authorization");

        if(isValidToken(accessToken)){
            return true;
        }

        String refreshToken = extractRefreshToken(request);
        if(refreshToken==null){
            throw new TokenIsNotValidException();
        }

        authService.validateToken(refreshToken);
        throw new AccessTokenRenewException();
    }

    private boolean isValidToken(String token){
        try{
            authService.validateToken(token);
            return true;
        }catch (UnAuthorizedException e){
            throw new UnAuthorizedException();
        }
    }

    private String extractRefreshToken(HttpServletRequest request){
        Cookie cookie = WebUtils.getCookie(request, "REFRESH_TOKEN");
        if(cookie==null){
            return null;
        }
        return cookie.getValue();
    }
}

interceptor 에 정의되어 있는 모든 소스들을 가져와서 보여주었지만 이번 포스팅에 해당되는 내용은

if(handler instanceof HandlerMethod) {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    NoAuth noAuth = handlerMethod.getMethodAnnotation(NoAuth.class);
    if(noAuth != null){
        return true;
    }
}

이 부분이 핵심입니다. HandlerMethod 로 치환해준 후에 NoAuth 라는 어노테이션을 가지고 있는지 확인합니다.

만약 가지고 있다면 바로 true 를 return 하여 이후에 존재하는 로직을 수행하지 않습니다.

 

결과

직접 한번 실행해보도록 하겠습니다.

interceptor 에 breaking pointer 를 걸어두고 인텔리제이 .http 파일을 사용해서 회원가입 API 를 호출하였습니다.

noAuth 가 null 이 아닌 것을 확인할 수 있습니다.

바로 Controller 로 요청이 넘어온걸 확인할 수 있죠????

 

마무리

이렇게 오늘은 같은 uri 이지만 http method 에 따라 interceptor 에서 구분해주는 방법을 한번 알아보았습니다.

사실 실무에서는 요새 많이들 spring security 를 사용한다고 알고 있습니다.

 

spring security 를 사용하게 되면 저런 번거로움 없이 http method 에 따라서 path 를 제외시켜 줄 수 있는 기능을 제공해준다고 합니다.

 

아직 security 를 재대로 학습하지 않아서 저기까지의 내용은 정확하게 알 수 없지만 다음 목표가 spring security 를 학습해보는 것 입니다.

현재 회사에서도 최근에 spring boot 로 되어 있는 프로젝트를 하나 더 맡게 되었는데 security 가 적용되어 있어 더이상은 학습을 미룰 수 없겠더라구요....

 

어쨌든 혹시나 저와 같은 고민을 하시는 분들께 조금이나마 도움이 되었으면 하는 바람입니다.

 

(이렇게 해결할 수 있다고 친절하게 설명해주신 개발바닥2사로 ㄱㅈㅎ 님께 진심으로 감사함을 표하는 바입니다.)