Merge pull request #44 from beaniejoy/feature/43

Http Request, Response 관련 logging filter 적용
This commit is contained in:
Hanbin Lee
2023-04-15 21:31:00 +09:00
committed by GitHub
21 changed files with 232 additions and 27 deletions

View File

@@ -67,6 +67,8 @@ subprojects {
// Logging
implementation("io.github.microutils:kotlin-logging:${Version.Deps.KOTLIN_LOGGING}")
implementation("com.google.code.gson:gson")
// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")

View File

@@ -1,9 +1,9 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import io.beaniejoy.dongnecafe.model.TokenResponse
import io.beaniejoy.dongnecafe.service.AuthService

View File

@@ -1,6 +1,7 @@
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException

View File

@@ -4,6 +4,7 @@ import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetailsService

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.utils.getAuthPrincipal
import io.beaniejoy.dongnecafe.utils.security.getAuthPrincipal
import mu.KLogging
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

View File

@@ -0,0 +1,50 @@
package io.beaniejoy.dongnecafe.infra.logging
import io.beaniejoy.dongnecafe.utils.logging.*
import org.springframework.http.HttpStatus
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
data class HttpLogMessage(
val httpMethod: String,
val requestUri: String,
val httpStatus: HttpStatus,
val clientIp: String,
val elapsedTime: Double,
val headers: String?,
val requestParam: String?,
val requestBody: String?,
val responseBody: String?,
) {
companion object {
fun createInstance(
requestWrapper: ContentCachingRequestWrapper,
responseWrapper: ContentCachingResponseWrapper,
elapsedTime: Double
): HttpLogMessage {
return HttpLogMessage(
httpMethod = requestWrapper.method,
requestUri = requestWrapper.requestURI,
httpStatus = HttpStatus.valueOf(responseWrapper.status),
clientIp = requestWrapper.getClientIp(),
elapsedTime = elapsedTime,
headers = requestWrapper.getRequestHeaders(),
requestParam = requestWrapper.getRequestParams(),
requestBody = requestWrapper.getRequestBody(),
responseBody = responseWrapper.getResponseBody(),
)
}
}
fun toPrettierLog(): String {
return """
|
|[REQUEST] ${this.httpMethod} ${this.requestUri} ${this.httpStatus} (${this.elapsedTime})
|>> CLIENT_IP: ${this.clientIp}
|>> HEADERS: ${this.headers}
|>> REQUEST_PARAM: ${this.requestParam}
|>> REQUEST_BODY: ${this.requestBody}
|>> RESPONSE_BODY: ${this.responseBody}
""".trimMargin()
}
}

View File

@@ -0,0 +1,57 @@
package io.beaniejoy.dongnecafe.infra.logging
import mu.KotlinLogging
import org.slf4j.MDC
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class ReqResLoggingFilter : OncePerRequestFilter() {
private val log = KotlinLogging.logger {}
companion object {
const val REQUEST_ID = "request_id"
}
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
val cachingRequestWrapper = ContentCachingRequestWrapper(request)
val cachingResponseWrapper = ContentCachingResponseWrapper(response)
val requestId = UUID.randomUUID().toString().substring(0, 8)
MDC.put(REQUEST_ID, requestId)
val startTime = System.currentTimeMillis()
filterChain.doFilter(cachingRequestWrapper, cachingResponseWrapper)
val end = System.currentTimeMillis()
try {
log.info {
HttpLogMessage.createInstance(
requestWrapper = cachingRequestWrapper,
responseWrapper = cachingResponseWrapper,
elapsedTime = (end - startTime) / 1000.0
).toPrettierLog()
}
cachingResponseWrapper.copyBodyToResponse()
} catch (e: Exception) {
log.error(e) { "[${this::class.simpleName}] Logging 실패" }
}
MDC.remove(REQUEST_ID)
}
}

View File

@@ -0,0 +1,17 @@
package io.beaniejoy.dongnecafe.infra.logging.constant
/**
* http request client ip possible enum list
* (ref. https://blog.yevgnenll.me/posts/find-client-ip-from-http-request-header)
* @property headerName String client ip header name
*/
enum class HttpClientIp(
val headerName: String,
) {
X_FORWARDED_FOR("X-Forwarded-For"),
PROXY_CLIENT_IP("Proxy-Client-IP"),
WL_PROXY_CLIENT_IP("WL-Proxy-Client-IP"),
HTTP_X_FORWARDED("HTTP_X_FORWARDED"),
HTTP_X_FORWARDED_FOR("HTTP_X_FORWARDED_FOR"),
HTTP_CLIENT_IP("HTTP_CLIENT_IP")
}

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security
package io.beaniejoy.dongnecafe.infra.security
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import org.springframework.security.core.GrantedAuthority

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.security.config
package io.beaniejoy.dongnecafe.infra.security.config
import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
import io.beaniejoy.dongnecafe.infra.security.filter.JwtAuthenticationFilter
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security.constant
package io.beaniejoy.dongnecafe.infra.security.constant
object SecurityConstant {
const val BEARER = "Bearer"

View File

@@ -1,8 +1,8 @@
package io.beaniejoy.dongnecafe.security.filter
package io.beaniejoy.dongnecafe.infra.security.filter
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.BEARER
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.WHITESPACE
import mu.KotlinLogging
import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security.handler
package io.beaniejoy.dongnecafe.infra.security.handler
import mu.KLogging
import org.springframework.security.access.AccessDeniedException

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security.handler
package io.beaniejoy.dongnecafe.infra.security.handler
import mu.KLogging
import org.springframework.security.core.AuthenticationException

View File

@@ -0,0 +1,45 @@
package io.beaniejoy.dongnecafe.utils.logging
import com.google.gson.Gson
import io.beaniejoy.dongnecafe.infra.logging.constant.HttpClientIp
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import javax.servlet.http.HttpServletRequest
fun HttpServletRequest.getRequestHeaders(): String? {
val request = this
return Gson().toJson(
mutableMapOf<String, String?>().apply {
request.headerNames.toList().forEach {
this[it] = request.getHeader(it)
}
}
)
}
fun HttpServletRequest.getRequestParams(): String {
return this.parameterMap.mapValues {
it.value.joinToString(",")
}.entries.joinToString("&")
}
fun HttpServletRequest.getClientIp(): String {
HttpClientIp.values().forEach { clientIpHeader ->
this.getHeader(clientIpHeader.headerName).also {
if (it.isNullOrBlank().not() && "unknown".equals(it, true).not()) {
return it
}
}
}
return this.remoteAddr
}
fun ContentCachingRequestWrapper.getRequestBody(): String {
return this.contentAsByteArray.toString(Charsets.UTF_8)
}
// TODO: logging response body maximum size 고려
fun ContentCachingResponseWrapper.getResponseBody(): String {
return this.contentAsByteArray.toString(Charsets.UTF_8)
}

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.security.utils
package io.beaniejoy.dongnecafe.utils.security
import io.beaniejoy.dongnecafe.security.SecurityUser
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.JWT_AUTHORITIES_KEY
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.JWT_AUTHORITIES_KEY
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.security.utils
package io.beaniejoy.dongnecafe.utils.security
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.ANONYMOUS_USER
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.ROLE_ANONYMOUS
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ANONYMOUS_USER
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ROLE_ANONYMOUS
import org.springframework.security.core.Authentication
fun Authentication.getAuthPrincipal() : String? {

View File

@@ -1,9 +1,9 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.security.utils.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint
import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean

View File

@@ -14,6 +14,9 @@ spring:
devtools:
livereload:
enabled: false # no use devtools' LiveReload Server
security:
filter:
order: 10
logging:
level:

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- Pattern -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5level) [%15.15t] [%X{request_id}] %clr(%-40.40logger{39}){cyan} : %m%n%wEx"/>
<!-- Request Thread Console Appender -->
<appender name="THREAD_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<springProfile name="local">
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe">
<appender-ref ref="THREAD_CONSOLE"/>
</logger>
<!-- Bootstrap class file -->
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe.DongneServiceApiApplicationKt">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>