avatar
Octoping Blog

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

어노테이션으로 로그인 체크 쉽게 하기!
SpringInterceptorKotlinJava
7 months ago
·
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 에러와 함께 메시지가 떨어져나오는 모습을 볼 수 있다~

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


- 컬렉션 아티클






반갑습니다 😄