반응형

https://github.com/Seungkyu-Han/Toge-do-backend

 

GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다

Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.

github.com

이번 프로젝트는 Gateway를 사용하기 때문에 각각의 server에서 security를 설정하지 않고, Gateway에서 Jwt 토큰을 확인하여 헤더를 변경해 각 서버로 재요청하도록 설계해보았다.

 

그렇게 작성한 GatewayFilter는 다음과 같다.

@Component
class AuthorizationFilter(
    private val jwtTokenProvider: JwtTokenProvider
) : AbstractGatewayFilterFactory<AuthorizationFilter.Config>(Config::class.java) {

    class Config

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            val authHeader = exchange.request.headers["Authorization"]?.firstOrNull()

            if (!isValidAuthorizationHeader(authHeader)) {
                return@GatewayFilter forbid(exchange)
            }

            val token = authHeader!!.removePrefix("Bearer ")

            if (!jwtTokenProvider.isAccessToken(token)) {
                return@GatewayFilter forbid(exchange)
            }

            try {
                val userId = jwtTokenProvider.getUserId(token)
                val nextReq = exchange.request.mutate()
                    .header("X-VP-UserId", userId)
                    .build()

                chain.filter(exchange.mutate().request(nextReq).build())
            } catch (jwtException: JwtException) {
                forbid(exchange)
            }
        }
    }

    private fun isValidAuthorizationHeader(authHeader: String?): Boolean {
        return authHeader != null && authHeader.startsWith("Bearer ")
    }

    private fun forbid(exchange: ServerWebExchange): Mono<Void> {
        exchange.response.statusCode = HttpStatus.FORBIDDEN
        return exchange.response.setComplete()
    }
}

 

헤더를 확인하여 헤더가 비어 있거나, Bearer 로 시작하지 않으면 403을 반환한다.

토큰들이 유효하지 않은 경우에도 403을 반환한다.

 

이렇게 filter를 만들고 보니 테스트를 해보고 싶어졌다.

 

하지만 이 apply 함수를 어떻게 테스트하는지 몰랐고, 찾아보던 중 MockServerHttpRequestMockServerWebExchange를 발견했다.

 

이거로 ServerHttpRequest, ServerWebExchange를 생성하고 이걸 넘겨주면 되는 것이다.

 

    @Test
    @DisplayName("Token이 Bearer로 시작하지 않는 경우")
    fun authorizationIsNotStartWithBearerReturnForbidden(){
        //given
        val request = MockServerHttpRequest.get("/")
            .header(HttpHeaders.AUTHORIZATION, "NOT BEARER!!")

        val exchange = MockServerWebExchange.from(request)
        val filter = authorizationFilter.apply(AuthorizationFilter.Config())

        //when
        StepVerifier.create(filter.filter(exchange) { _ -> Mono.empty() })
            .verifyComplete()

        //then
        Assertions.assertEquals(HttpStatus.FORBIDDEN, exchange.response.statusCode)
    }

 

우선 가장 먼저 토큰이 없는 경우이다.

Authorization 헤더가 비어있기 때문에 바로 403이 반환된다.

 

    @Test
    @DisplayName("토큰이 만료된 경우")
    fun tokenIsExpiredReturnForbidden(){
        //given
        val token = "i am token!"
        val request = MockServerHttpRequest.get("/")
            .header(HttpHeaders.AUTHORIZATION, "Bearer $token")

        val exchange = MockServerWebExchange.from(request)
        val filter = authorizationFilter.apply(AuthorizationFilter.Config())

        `when`(jwtTokenProvider.getUserId(token))
            .thenThrow(ExpiredJwtException::class.java)

        //when
        StepVerifier.create(filter.filter(exchange) { _ -> Mono.empty() })
            .verifyComplete()

        //then
        Assertions.assertEquals(HttpStatus.FORBIDDEN, exchange.response.statusCode)
    }

 

MockServerHttpRequest에 원하는 헤더를 넣을 수 있다.

어차피 어떤 토큰이든지 mocking한 jwtTokenProvider에서 원하는 값을 리턴하기 때문에 아무 값이나 넣어주었다.

 

Jwt를 Parse 할 때 발생하는 에러는 ExpiredJwtException, MalformedJwtException, SignatureException 이기 때문에 이 에러들을 mocking으로 부터 발생시켜 각각의 Response를 테스트 할 수 있었다.

+ Recent posts