/
/
    🖥 백엔드

    어노테이션으로 로그인한 유저만 api를 호출할 수 있도록 간단히 필터링하기

    어노테이션으로 로그인 체크 쉽게 하기!
    SpringInterceptorKotlinJava
    O
    Octoping
    2024.05.13
    ·
    5 min read

    스프링을 싫어하는 사람들이 많이 하는 말이 있다. 스프링은 어노테이션이 덕지덕지 붙어서 극혐이라고..

    하지만 어노테이션도 잘 쓰면 로직을 간단히 추상화해줄 수 있어서 좋은 점이 많다.

    어노테이션을 이용해서 로그인한 유저만 api를 호출할 수 있도록 하고, 미로그인 유저는 400 에러를 발생하도록 필터링하는 기능을 구현해보려고 한다.

    @NeedLogin
    @Operation(summary = "아티클 작성하기")
    @PostMapping("/{username}/articles")
    fun writeArticle(
        @PathVariable("username") username: String,
        @RequestBody request: BlogArticleWriteRequestDTO,
    ) {
        TODO("로직은 알아서 잘 딱 깔끔하게")
    }

    설명하기 앞서, 원하는 결과물은 이렇다.

    api의 위에 @NeedLogin 어노테이션을 붙이는 것 만으로 API가 로그인을 필요로 한다는 것을 명시적으로 나타내줄 수 있다.

    언어는 코틀린으로 짜여있지만, Java로도 코드가 크게 다르진 않을 것이니 따라가봅시다

    레츠고~


    @Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.FUNCTION)
    annotation class NeedLogin

    간단하게 어노테이션을 만들어주자. API가 실행될 때 로그인 상태를 검증하는 것이니 RUNTIME이고, 함수에 붙여줄 생각이니 타겟은 FUNCTION으로 한다.

    @Component
    class NeedLoginInterceptor(
        private val tokenProvider: TokenProvider,
    ) : HandlerInterceptor {
        override fun preHandle(
            request: HttpServletRequest,
            response: HttpServletResponse,
            handler: Any,
        ): Boolean {
            // NeedLogin 어노테이션이 붙어있지 않은 함수면 패스
            if (!hasNeedLoginAnnotation(handler)) {
                return true
            }
    
            // 로그인된 상태인지 체크
            if (!hasValidAccessToken(request)) {
                throw UserNotAuthorizedException()
            }
    
            return true
        }
    
        private fun hasValidAccessToken(request: HttpServletRequest): Boolean {
            return getAccessCookie(request)
                ?.let { tokenProvider.validateToken(it) }
                ?: false
        }
    
        private fun hasNeedLoginAnnotation(handler: Any): Boolean {
            return handler is HandlerMethod &&
                handler.method.isAnnotationPresent(NeedLogin::class.java)
        }
    }

    api가 호출될 때 마다 검증해야 하기 때문에 Interceptor를 사용해준다. HandlerInterceptor를 상속받아서 NeedLoginInterceptor라는 클래스를 만들었다.

    'api가 실행되기 전에' 로그인 상태를 검증해야 하므로 preHandle에 로직을 적어넣었다.

    NeedLogin 어노테이션이 붙어있는지 체크해서 없을 경우 패스하기 위해 리턴 시켜준다.

    그 후 각자의 방법으로 로그인 여부를 체크해준 후 미로그인 상태일 경우 Exception을 터트려주면 된다. 나는 AccessToken을 이용하는 방법을 사용했기 때문에 해당 방법으로 구현해주었다.

    @Configuration
    class WebConfig(
        private val needLoginInterceptor: NeedLoginInterceptor
    ) : WebMvcConfigurer {
        override fun addInterceptors(registry: InterceptorRegistry) {
            registry.addInterceptor(needLoginInterceptor)
                .addPathPatterns("/**")
        }
    }

    마지막으로는 인터셉터를 적용해주기 위해 WebMvcConfigurer에다가 내가 만든 인터셉터를 적용해주었다.

    그러면 끝!

    @NeedLogin
    @Operation(summary = "아티클 작성하기")
    @PostMapping("/{username}/articles")
    fun writeArticle(
        @PathVariable("username") username: String,
        @RequestBody request: BlogArticleWriteRequestDTO,
    ) {
        TODO("로직은 알아서 잘 딱 깔끔하게")
    }

    나머지는 이렇게 api 위에다가 NeedLogin 어노테이션만 붙여주면 된다.

    @RestControllerAdvice
    class GlobalExceptionHandler {
        @ExceptionHandler(UserNotAuthorizedException::class)
        fun handleUserNotAuthorizedException(exception: UserNotAuthorizedException): ResponseEntity<ErrorResponse> {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(ErrorResponse(exception.message))
        }
    }

    마지막으로 예외가 발생했을 때 400 에러와 exception 메시지를 출력해주기 위해 ExceptionHandler로 받아다가 원하는 에러 코드와 메시지를 전달해주면 된다.

    {
      "error": "로그인이 필요합니다."
    }

    그러면 이렇게 API를 실행했을 때 401 에러와 함께 메시지가 떨어져나오는 모습을 볼 수 있다~

    앞으로도 인터셉터와 어노테이션 기능을 잘 활용하여 로직을 잘 추상화하고 반복을 제거해보자







    - 컬렉션 아티클