ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Security의 인증을 커스텀 해보자.(2) - AuthenticationFilter, Authentication
    Spring Security 2023. 4. 27. 16:41

     이번 포스팅부터는 실제 우리 프로젝트에서 Spring Security를 어떻게 커스텀했는지 살펴볼 것이다.

     

     이전 시간에 잠깐 언급한 우리 프로젝트의 인증 요구사항에 대해서 다시 한번 살펴 보자.

     

    프로젝트 요구 사항

    • 하나의 프로젝트 내 2개의 인증을 포함한다.
    • Server에서 접속을 할 때는 Client-Id, Client-Secret를 이용한 Hmac 기반으로 인증을 처리한다.
    • FrontEnd에서 접속할 때는 JWT 토큰 기반으로 인증을 처리한다.

     

     우리 프로젝트는 대기열 서비스로 커머스 서비스와 같은 고객에게 줄 서기 기능을 제공한다. 고객이 우리 서버에 접속을 할 때, 고객의 FrontEnd에서 접근하는 것과 고객의 Server에서 접근하는 것을 고려해야 했다. 그리고 그 두 가지의 인증 방식은 보안과 편의성 측면에서 달라야 했다.

     고객의 Server는 우리 서버에서 발급받은 Client-Id, Client-Secret를 이용한 Hmac 기반으로 인증 요청이 요청하며, 고객의 FrontEnd는 우리의 서버를 통해서 발급된 JWT 기반으로 인증을 요청한다.

     그래서 이번 포스팅에서 살펴볼 내용은 Hmac 기반 인증을 위한 HmacAuthenticationFilter와 JWT 토큰 기반의 인증을 위한 JwtAuthenticationFilter에 관한 것이다.

    1. AuthenticationFilter

    1-1. CommonAuthenticationFilter

    • Hmac, JWT 인증의 공통처리를 담은 추상 클래스이다.
    • RequestMatcher를 통해서 요청 URL을 감시한다.
    • 인증 처리를 AuthenticationManager에 위임한다.
    • 인증이 완료된 후 Authentication을 SecurityContextHolder에 저장한다.
    • Authentication 객체를 만드는 역할은 해당 클래스를 상속받은 클래스에 위임한다.
    abstract class CommonAuthenticationFiller(
        protected val authenticationManager: AuthenticationManager,
        private val requestMatcher: RequestMatcher
    ) : OncePerRequestFilter() {
    
        override fun doFilterInternal(
            request: HttpServletRequest,
            response: HttpServletResponse,
            filterChain: FilterChain
        ) {
            try {
                // 요청 URL을 감시한다.
                if (!isRequestMatched(request)) {
                    filterChain.doFilter(request, response)
                    return
                }
    
                val result = attemptAuthentication(request, response) // 인증을 AuthenticationManager에 위임한다.
                SecurityContextHolder.getContext().authentication = result // 인증이 완료 된 후 Authentication을 SecurityContextHolder에 저장한다.
            } catch (e: AuthenticationException) {
                SecurityContextHolder.clearContext()
            }
            filterChain.doFilter(request, response)
        }
    
        private fun isRequestMatched(request: HttpServletRequest): Boolean {
            return requestMatcher.matches(request)
        }
    
        // Authentication 객체를 만드는 역할은 상속 받은 클래스에 위임한다.
        abstract fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication
    
    }

    1-2. HmacAuthenticationFilter

    Hmac 인증은 ClientSecret으로 전달하고자 하는 Message를 암호화한 HmacSignature를 서버에 전달하여 인증을 요청하는 방식이다.

    • 요청 URL이 /server/** 로 들어오는 요청을 처리한다.
    • HTTP Authorization Header로 들어온 ClientId:HmacSignature 형태의 값이 유효한치 체크한다.
    • ClientId, HmacSignature, Message를 토대로 Authentication(HmacAuthenticationToken)을 만든다.
      • 우리는 HTTP requestURL을 Message(Payload)로 채택했다.
    class HmacSignatureAuthenticationFilter(
        authenticationManager: AuthenticationManager,
        requestMatcher: RequestMatcher,
    ) : CommonAuthenticationFiller(authenticationManager, requestMatcher) {
    
        override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
            val authorization = request.getHeader(HttpHeaders.AUTHORIZATION)
    
            if (authorization.isNullOrEmpty() || !authorization.contains(":")) {
                throw BadCredentialsException("Invalid Hmac authentication : $authorization")
            }
    
            val (clientId, signature) = authorization.split(":")
            val authentication = HmacAuthenticationToken.unauthenticated(
                clientId = clientId,
                signature = signature,
                payload = request.requestURL.toString()
            )
    
            return authenticationManager.authenticate(authentication)
        }
    }

    1-3. JwtAuthenticationFilter

    • 요청 URL이 /client/** 로 들어오는 요청을 처리한다.
    • HTTP Authorization Header로 들어온 JWT 토큰 형태의 값이 유효한치 체크한다.
    • JWT토큰을 토대로 Authentication(JwtAuthenticationToken)을 만든다.
    class JwtAuthenticationFilter(
        authenticationManager: AuthenticationManager,
        requestMatcher: RequestMatcher,
    ) : CommonAuthenticationFiller(authenticationManager, requestMatcher) {
    
        companion object {
            private const val JWT_TOKEN_PREFIX = "Bearer "
        }
    
        override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
            val jwtToken = resolveJwtToken(request) ?: throw BadCredentialsException("Invalid JWT authentication")
    
            val jwtAuthenticationToken = JwtAuthenticationToken.unauthenticated(jwtToken)
            return authenticationManager.authenticate(jwtAuthenticationToken)
        }
    
        private fun resolveJwtToken(request: HttpServletRequest): String? {
            val header = request.getHeader(HttpHeaders.AUTHORIZATION)
            if (!header.isNullOrEmpty() && header.startsWith(JWT_TOKEN_PREFIX)) {
                return header.substring(7)
            }
    
            return null
        }
    }

    2. Authentication

    Authentication은 Security의 거의 모든 필터에서 필요하므로 Spring Security는 Authentication의 Interface로 기능구현을 강제하고 있다. 그리고 개발자가 쉽게 커스텀할 수 있도록 추상 클래스인 AbstractAuthenticationToken을 제공한다. 우리는 이 AbstractAuthenticationToken을 상속받아 구현을 했다.

    우리의 커스텀 목적은 실제 인증을 처리할 AuthenticationProvider에서 인증을 위해 필요한 값들을 Authentication을 통해 넘겨줄 필요가 있었으며, 인증을 처리 후 서비스 로직에서 인증 요청자를 알기 위해 Principal에 적절한 값을 넘겨줄 필요가 있었다.

    2-1. HmacAuthenticationToken

    • HmacAuthenticationFilter를 통해서 전달받은 clientId, signature, payload 값을 가지고 있다.
    • Principal을 위한 CommonPrincipal을 만들었고, clientId, roles(권한)을 담고 있다.
    • Static Factory Method를 이용하여 인증 준비 객체 생성(unauthenticated), 인증 완료 객체 생성(authenticated)을 담당하도록 했다.
    class HmacAuthenticationToken(
        val clientId: String? = null,
        val signature: String? = null,
        val payload: String? = null,
        val principal: CommonPrincipal? = null,
        authenticated: Boolean = false,
        authorities: MutableCollection<out GrantedAuthority> = mutableListOf()
    ) : AbstractAuthenticationToken(authorities) {
    
        init {
            isAuthenticated = authenticated
        }
    
        companion object {
            @JvmStatic
            fun unauthenticated(
                clientId: String,
                signature: String,
                payload: String
            ): HmacAuthenticationToken {
                return HmacAuthenticationToken(
                    clientId = clientId,
                    signature = signature,
                    payload = payload
                )
            }
    
            @JvmStatic
            fun authenticated(
                principal: CommonPrincipal,
                authorities: MutableCollection<out GrantedAuthority> = mutableListOf()
            ): HmacAuthenticationToken {
                return HmacAuthenticationToken(
                    clientId = principal.clientId,
                    authenticated = true,
                    principal = principal,
                    authorities = authorities
                )
            }
        }
    
        override fun getCredentials(): Any? {
            return null
        }
    
        override fun getPrincipal(): Any? {
            return this.principal
        }
    }
    
    // Custom Principal
    data class CommonPrincipal(
        val clientId: String,
        val userId: String? = null,
        val roles: List<Role>
    )

    2-2. JwtAuthenticationToken

    • JwtAuthenticationFilter를 통해서 전달받은 JwtToken 값을 가지고 있다.
    • Principal을 위한 CommonPrincipal을 만들었고, clientId, userId, roles(권한)을 담고 있다.
    • Static Factory Method를 이용하여 인증 준비 객체 생성(unauthenticated), 인증 완료 객체 생성(authenticated)을 담당하도록 했다.

     해당 코드는 위의 HmaAuthenticationToken과 동일한 패턴을 가지고 있다. 자세한 코드는 아래의 링크를 통해서 확인해 볼 수 있다.

    JwtAuthenticationToken.kt

     

    GitHub - f-lab-edu/inqueue: 커머스 서비스를 위한 줄서기 플랫폼 + 비실물 커머스 플랫폼

    커머스 서비스를 위한 줄서기 플랫폼 + 비실물 커머스 플랫폼. Contribute to f-lab-edu/inqueue development by creating an account on GitHub.

    github.com

     

     이번 포스팅을 통해서 우리는 AuthenticationFilter 및 Authentication을 커스텀하는 방법에 대해서 살펴보았으며, 다음 포스팅에서는 AuthenticationManager 구성 및 AuthenticationProvider의 커스텀 과정을 살펴볼 것이다. 

     

     다음 포스팅 내용의 코드를 먼저 살펴보고 싶다면, 위의 Github를 통해서 살펴볼 수 있다.

Designed by Tistory.