본문 바로가기
프로그래밍/기타

[Spring Security] 인증 Filter를 기준으로 Custom Filter 추가와 변경

by 사바라다 2023. 1. 18.

안녕하세요. 오늘 포스팅에서는 인증 Filter의 추가와 변경하는 방법에 대해서 알아보도록 하겠습니다.

Filter

Spring Securtiy는 기본적으로 Filter를 기반으로 동작합니다. 따라서 Spring Security Framework를 이해하기 위해서는 Filter에 대해서 이해하는것이 필수입니다. Filter는 요청을 수신하고 그 논리를 실행하고 최종적으로 다음 Filter로 요청을 전달하는 역할을 합니다.

아래의 이미지는 여러 필터가 동작하는 방식을 나타냅니다. Filter 1에서 로직을 실행하고 Filter 2로 전달, Filter 3으로 전달하고 마지막에는 Controller에서 Request를 받아서 처리합니다. 그리고 Controller의 Response가 만들어지고 이렇게 만들어진 Response는 Filter의 역 방향으로 처리됩니다. 즉, Filter 3, Filter 2 마지막에는 Filter 1을 거쳐서 호출했던 Client에게 응답되게 되는 구조를 가집니다.

이렇게 필터가 작동하는 순서가 정의된 필터의 모음을 필터 체인이라고합니다. 그리고 이렇게 모인 Filter들의 순서는 Order로 정수로 관리합니다. Order 숫자는 작은 수록 Request 앞쪽에 배치되는 필터입니다. 또한 동일한 Order 숫자를 가진 필터의 호출 순서는 확정적이지 않기 때문에 그때그때 다를 수 있음을 유념해야한다.

Filter 생성

Spring Security에서 사용하는 Filter는 Servlet에서 사용하는 필터와 동일합니다. javax.servlet.Filter 인터페이스를 구현해서 커스텀 Filter를 구현할 수 있습니다. 오늘 예제에서는 Request와 Response를 로깅하는 Logging Filter와 특정 Header의 유무를 판단하는 Validator Filter 두개입니다. 구현은 아래와 같이 할 수 있습니다.

아래 코드는 Logging Filter입니다. Client의 Request와 Response를 로깅하는 Filter입니다.

class LoggingFilter : Filter {

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {

        val httpServletRequest = request as HttpServletRequest
        val httpServletResponse = response as HttpServletResponse

        println("[REQ] ${httpServletRequest.method}  ${httpServletRequest.servletPath}")

        val measureTimeMillis = measureTimeMillis {
            chain.doFilter(request, response)
        }

        println("[RES] ${httpServletResponse.status}, $measureTimeMillis ms")
    }

}

아래 코드는 Header를 Validation 할 수 있는 Filter입니다. Request-Id Header가 없다면 400 에러를 반환하고 Header가 있다면 다음 Filter 또는 Controller로 요청을 위임합니다.

class RequestValidationFilter : Filter {

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {

        val httpRequest: HttpServletRequest = request as HttpServletRequest
        val httpResponse: HttpServletResponse = response as HttpServletResponse

        val requestId = httpRequest.getHeader("Request-Id")

        if (requestId == null || requestId.isBlank()) {
            httpResponse.status = HttpServletResponse.SC_BAD_REQUEST
            return
        }

        chain.doFilter(request, response)
    }
}

Pre & Post Filter 등록

이렇게 만들어진 Filter들은 Spring Security Config에 아래처럼 등록할 수 있습니다. 필터의 등록은 인증의 기본이 되는 Filter의 앞에 붙일지 아니면 뒤에 붙일지를 선택할 수 있습니다. addFilterBeforeaddFilterAfter를 이용하면 됩니다. addFilterBefore를 이용한다면 인증 필터 앞에 Filter가 위치하게되고 addFilterAfter를 이용한다면 인증 필터 뒤에 위치하게 됩니다.

override fun configure(http: HttpSecurity) {
        http
            .addFilterBefore(
                LoggingFilter(),
                BasicAuthenticationFilter::class.java
            )
            .addFilterAfter(
                RequestValidationFilter(),
                BasicAuthenticationFilter::class.java
            )
    }

Pre & Post Filter 테스트

테스트 Authentication Pre Filter로 LoggingFilter를 추가했고 post Filter로 RequestValidationFilter를 추가했습니다. 그리고 인증 필터는 BasicAuthenticationFilter를 사용합니다. 먼저 아래테스트를 해보겠습니다.

curl -i -u koangho93@naver.com:1234 -X GET localhost:8080/example

결과는 아래와 같습니다. 먼저 LoggingFilter에 의한 Logging이 된 것을 확인할 수 있고 401 또는 403 에러가 아니기 때문에 BasicAuthenticationFilter 또한 넘어간 것으 알 수 있습니다. 400으로 떨어진 것으로보아 RequestValidationFilter의 httpResponse.status = HttpServletResponse.SC_BAD_REQUEST로 반환되었다는 것을 알 수 있습니다.

[REQ] GET  /example
[RES] 400, 0 ms

해당 필터를 통과하기 위해서 -H "Request-Id: application/json" Header를 추가해서 Request를 전송합니다. 그렇게 했을 시 200으로 통과하는 것을 확인하실 수 있습니다.

curl -i -u koangho93@naver.com:1234 -H "Request-Id: application/json" -X GET localhost:8080/example
[REQ] GET  /example
[RES] 200, 44 ms

Authentication Filter 생성 및 변경

인증 필터의 앞뒤뿐만 아니라 인증 필터역시 Filter를 이용하며 커스터마이징하고 교체할 수 있습니다. 아래와 같은 StaticKey로 인증하는 Filter를 만들어보겠습니다. 해당 Filter는 Authorization Header에 sabarada라고 입력했는지 판별하고 정확한 값을 입력해야 통과시켜주는 필터입니다.

class StaticKeyAuthenticationFilter : Filter {

    private val authorizationKey = "sabarada"

    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {

        val httpServletRequest = request as HttpServletRequest
        val httpServletResponse = response as HttpServletResponse

        val authentication = httpServletRequest.getHeader("Authorization")

        if (authentication == authorizationKey) {
            chain.doFilter(request, response)
        } else {
            httpServletResponse.status = HttpServletResponse.SC_BAD_REQUEST
        }
    }

}

그리고 addFilterAt을 이용하여 BasicAuthenticationFilter의 위치 필터를 StaticKeyAuthenticationFilter으로 변경해줍니다. 이렇게 하면 BasicAuthenticationFilter 필터 대신 스스로 만든 StaticKeyAuthenticationFilter을 인증필터로써 사용할 수 있게 됩니다.

override fun configure(http: HttpSecurity) {
        http
            .addFilterBefore(
                LoggingFilter(),
                BasicAuthenticationFilter::class.java
            )
            .addFilterAfter(
                RequestValidationFilter(),
                BasicAuthenticationFilter::class.java
            )
            .addFilterAt(
                StaticKeyAuthenticationFilter(),
                BasicAuthenticationFilter::class.java
            )
    }

테스팅

테스트는 아래와 같습니다. -u koangho93@naver.com:1234으로 인증하지만 통과되지 않는것을 확인할 수 있습니다.

curl -i -u koangho93@naver.com:1234 -H "Request-Id: application/json" -X GET localhost:8080/example
HTTP/1.1 400
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 15 Jan 2023 15:37:14 GMT
Connection: close

그리고 -H "Authorization: sabarada" 인증으로 통과하는것을 아래 테스트로 확인하실수 있습니다.

curl -i -H "Request-Id: application/json" -H "Authorization: sabarada" -X GET localhost:8080/example
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Sun, 15 Jan 2023 15:36:22 GMT

마무리

오늘은 커스텀 필터를 작성하는 방법과 이렇게 생성한 필터를 Spring Security의 방식으로 추가하는 방법을 확인해보았습니다.

감사합니다.

참조

[1] Spring Security In Action

댓글