Spring Framework/Filter

Spring Filter Header 인가 처리 - Spring Filter Hands On 2

Junuuu 2023. 10. 5. 00:01

개요

Spring Filter의 정의에 이어 실습으로 컨트롤러를 구성하고 헤더를 받아 컨트롤러 별 인가를 처리해보려고 합니다.

해당 방법 이외에 Spring Security를 활용할 수도 있습니다.

Filter에서 범위가 조금 벗어난다고 생각이 들지만 다음 챕터에서 한번 다루어 보려고 합니다.

 

Controller 세팅

@RestController
class AuthorizationTestController {
    @GetMapping("/admin-path")
    fun onlyAdmin(@RequestHeader role: String): String{
        return "접근 가능"
    }

    @GetMapping("/user-path")
    fun onlyUser(@RequestHeader role: String): String{
        return "접근 가능"
    }
}

role이란 헤더를 받는 2개의 Endpoint를 만들었습니다.

해당 상황은 이미 Gateway의 람다로 토큰이 파싱되었고 Role이란 헤더에 역할이 담겨서 오는 것을 가정했습니다.

 

AuthorizationFilter 구현

@Component
class AuthorizationFilter: OncePerRequestFilter() {
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val requestURI = request.requestURI
        val roleHeader = request.getHeader("role")
        if(isAdminPath(requestURI) && isNotAdmin(roleHeader)){
            throw RuntimeException("현재 권한으로는 접근할 수 없는 uri입니다.")
        }
        chain.doFilter(request, response)
    }

    private fun isAdminPath(requestURI: String): Boolean{
        return PatternMatchUtils.simpleMatch(userCanAccessPaths, requestURI) == false
    }

    private fun isNotAdmin(role: String): Boolean{
        return role != "admin"
    }

    companion object{
        val userCanAccessPaths = listOf("/user-path","/user-path1","/user-path2").toTypedArray()
    }
}

OncePerRequestFilter를 상속받아 구현합니다.

OncePerRequestFilter는 하나의 request 당 단일 실행을 보장합니다. (포워딩이 발생하면 필터체인이 다시 동작합니다)

OncePerRequestFilter는 추상클래스로 GenericFilterBean을 구현하고 있습니다.

public abstract class OncePerRequestFilter extends GenericFilterBean

 

추상클래스에서 구현되지 않은 doFilterInternal 메서드를 구현해주어야 합니다.

이때 requestURI(요청이온 URI), getHeader메서드를 통해 접근이 가능한 path를 정의하고 제어할 수 있습니다.

이때 주의해야 할 점으로 getHeader시 값이 없으면 null이 들어옵니다.

 

URI 매칭에는 PatternMatchUtils를 활용했습니다.

PatternMatchUtils는 스프링에서 지원해주는 기능이며 * 패턴을 활용하여 문자열이 일치하는지 확인합니다.

 

구현한 필터 등록하기

@Configuration
class FilterConfig{
    @Bean
    fun authorizationFilterRegistration(authorizationFilter: AuthorizationFilter): FilterRegistrationBean<*> {
        val filterRegistrationBean = FilterRegistrationBean<Filter>()
        filterRegistrationBean.filter = authorizationFilter
        filterRegistrationBean.order = 1
        filterRegistrationBean.addUrlPatterns("/*")
        return filterRegistrationBean
    }
}

FilterRegistration을 빈으로 등록하여 필터를 등록합니다.

이때 addUrlPatterns를 활용하여 모든 경로를 지정했습니다.

exclusivePatterns가 지원되지 않는점은 아쉽습니다.

 

주의사항 1 - ControllerAdvice에서 예외를 잡지 못한다.

  override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
    val result = runCatching {
      val requestURI = request.requestURI
      val role: String = request.getHeader(ROLE) ?: throw AuthorizationException(AUTHORIZATION_EXCEPTION_MESSAGE)
      if (isAdminPath(requestURI) && isNotAdmin(role)) {
        throw AuthorizationException(AUTHORIZATION_EXCEPTION_MESSAGE)
      }
      chain.doFilter(request, response)
    }
    if(result.isFailure){
      response.resetBuffer()
      response.status = HttpServletResponse.SC_UNAUTHORIZED;
      response.writer.write(AUTHORIZATION_EXCEPTION_MESSAGE);
    }
  }

Spring MVC에서 발생항 예외라면 HandlerExceptionResolver가 처리하지만 Filter는 Dispatcher Servlet에서 처리하기 전에 예외가 발생되기 때문에 따로 예외를 잡지 않으면 500 에러가 발생합니다.

 

이를 대비하기 위해 runCatching으로 예외를 잡아서 실패한 경우에는 401 상태코드를 반환합니다.

 

 

 

주의사항2 - Health Check 

위에서 모든 UrlPatterns에 대해서 인증을 수행해 버렸기 때문에 헬스체크를 수행하는 곳에서도 401 인증예외가 발생하였고, argoCD의 k8s pod가 뜨지 않는 상황이 발생했었습니다.

이를 막기위해 인증이 필요한 url들만 패턴으로 모두 등록해 주었습니다.