Skip to content

Commit

Permalink
feat: 주요 API 로깅 구현 (#239)
Browse files Browse the repository at this point in the history
* feat: 로깅을 위한 RequestInfo 관련 클래스 추가

* feat: Admin/Auth/Verification API 로깅 추가
  • Loading branch information
23tae authored Dec 4, 2024
1 parent 5226228 commit 5b04279
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 30 deletions.
28 changes: 21 additions & 7 deletions src/main/kotlin/uoslife/servermeeting/admin/api/AdminApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
Expand All @@ -17,6 +18,7 @@ import uoslife.servermeeting.admin.dto.request.ResetEmailRequest
import uoslife.servermeeting.admin.service.AdminService
import uoslife.servermeeting.global.config.SwaggerConfig
import uoslife.servermeeting.global.error.ErrorResponse
import uoslife.servermeeting.global.util.RequestUtils
import uoslife.servermeeting.payment.dto.response.PaymentResponseDto

@RestController
Expand Down Expand Up @@ -50,7 +52,10 @@ import uoslife.servermeeting.payment.dto.response.PaymentResponseDto
)]
)]
)
class AdminApi(private val adminService: AdminService) {
class AdminApi(
private val adminService: AdminService,
private val requestUtils: RequestUtils,
) {
@Operation(summary = "이메일 발송 횟수 초기화", description = "특정 이메일의 일일 발송 횟수를 초기화합니다.")
@ApiResponses(
value =
Expand All @@ -63,8 +68,12 @@ class AdminApi(private val adminService: AdminService) {
]
)
@PostMapping("/verification/send-email/reset")
fun resetEmailSendCount(@RequestBody request: ResetEmailRequest): ResponseEntity<Unit> {
adminService.resetEmailSendCount(request.email)
fun resetEmailSendCount(
request: HttpServletRequest,
@RequestBody body: ResetEmailRequest
): ResponseEntity<Unit> {
val requestInfo = requestUtils.toRequestInfoDto(request)
adminService.resetEmailSendCount(body.email, requestInfo)
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

Expand Down Expand Up @@ -95,10 +104,12 @@ class AdminApi(private val adminService: AdminService) {
)
@DeleteMapping("/user")
fun deleteUser(
@RequestBody request: DeleteUserRequest,
@RequestBody body: DeleteUserRequest,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<Unit> {
adminService.deleteUserById(request.userId, response)
val requestInfo = requestUtils.toRequestInfoDto(request)
adminService.deleteUserById(body.userId, response, requestInfo)
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

Expand Down Expand Up @@ -144,7 +155,10 @@ class AdminApi(private val adminService: AdminService) {
]
)
@PostMapping("/refund/match")
fun refundPayment(): ResponseEntity<PaymentResponseDto.NotMatchedPaymentRefundResponse> {
return ResponseEntity.status(HttpStatus.OK).body(adminService.refundPayment())
fun refundPayment(
request: HttpServletRequest
): ResponseEntity<PaymentResponseDto.NotMatchedPaymentRefundResponse> {
val requestInfo = requestUtils.toRequestInfoDto(request)
return ResponseEntity.status(HttpStatus.OK).body(adminService.refundPayment(requestInfo))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import uoslife.servermeeting.global.common.dto.RequestInfoDto
import uoslife.servermeeting.payment.dto.response.PaymentResponseDto
import uoslife.servermeeting.payment.service.PaymentService
import uoslife.servermeeting.user.service.UserService
Expand All @@ -22,21 +23,26 @@ class AdminService(
private val logger = LoggerFactory.getLogger(AdminService::class.java)
}

fun resetEmailSendCount(email: String) {
fun resetEmailSendCount(email: String, requestInfo: RequestInfoDto) {
val sendCountKey =
VerificationUtils.generateRedisKey(VerificationConstants.SEND_COUNT_PREFIX, email, true)

redisTemplate.opsForValue().set(sendCountKey, "0")
redisTemplate.expire(sendCountKey, Duration.ofDays(1))

logger.info("[이메일 발송 횟수 초기화] email: $email")
logger.info("[ADMIN-이메일 발송 횟수 초기화] targetEmail: $email, $requestInfo")
}

fun deleteUserById(userId: Long, response: HttpServletResponse) {
fun deleteUserById(userId: Long, response: HttpServletResponse, requestInfo: RequestInfoDto) {
userService.deleteUserById(userId, response)
logger.info("[ADMIN-유저 삭제] targetUserId: $userId, $requestInfo")
}

fun refundPayment(): PaymentResponseDto.NotMatchedPaymentRefundResponse {
return paymentService.refundPayment()
fun refundPayment(
requestInfo: RequestInfoDto
): PaymentResponseDto.NotMatchedPaymentRefundResponse {
val result = paymentService.refundPayment()
logger.info("[ADMIN-매칭 실패 유저 환불] $requestInfo")
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import uoslife.servermeeting.global.auth.exception.*
import uoslife.servermeeting.global.auth.security.JwtTokenProvider
import uoslife.servermeeting.global.auth.security.SecurityConstants
import uoslife.servermeeting.global.auth.util.CookieUtils
import uoslife.servermeeting.global.util.RequestUtils

@Service
class AuthService(
private val jwtTokenProvider: JwtTokenProvider,
private val cookieUtils: CookieUtils,
private val requestUtils: RequestUtils,
@Value("\${jwt.refresh.expiration}") private val refreshTokenExpiration: Long,
) {
companion object {
Expand All @@ -43,15 +45,19 @@ class AuthService(
}

fun reissueTokens(request: HttpServletRequest, response: HttpServletResponse): JwtResponse {
val requestInfo = requestUtils.toRequestInfoDto(request)
val refreshToken =
cookieUtils.getRefreshTokenFromCookie(request)
?: throw JwtRefreshTokenNotFoundException()

?: run {
logger.warn("[재발급 실패(토큰없음)] $requestInfo")
throw JwtRefreshTokenNotFoundException()
}
try {
val userId = jwtTokenProvider.getUserIdFromRefreshToken(refreshToken)

val storedToken = jwtTokenProvider.getStoredRefreshToken(userId)
if (storedToken != refreshToken) {
logger.warn("[재발급 실패(재사용)] $requestInfo")
throw JwtRefreshTokenReusedException()
}

Expand All @@ -61,26 +67,30 @@ class AuthService(
jwtTokenProvider.saveRefreshToken(userId, newRefreshToken)

cookieUtils.addRefreshTokenCookie(response, newRefreshToken, refreshTokenExpiration)
logger.info("[토큰 재발급 성공] USER ID: $userId")
logger.info("[재발급 성공] userId: $userId")
return JwtResponse(newAccessToken)
} catch (e: ExpiredJwtException) {
logger.warn("[재발급 실패(만료)] $requestInfo")
throw JwtRefreshTokenExpiredException()
} catch (e: JwtException) {
logger.warn("[재발급 실패(서명)] $requestInfo")
throw JwtTokenInvalidSignatureException()
}
}

fun logout(request: HttpServletRequest, response: HttpServletResponse) {
val requestInfo = requestUtils.toRequestInfoDto(request)

val refreshToken = cookieUtils.getRefreshTokenFromCookie(request)
if (refreshToken != null) {
try {
val userId = jwtTokenProvider.getUserIdFromRefreshToken(refreshToken)
jwtTokenProvider.deleteRefreshToken(userId)
} catch (e: JwtException) {
logger.warn("[로그아웃 요청] 유효하지 않은 리프레시 토큰으로 로그아웃 시도")
logger.warn("[로그아웃 요청] 유효하지 않은 리프레시 토큰 사용 $requestInfo")
}
} else {
logger.warn("[로그아웃 요청] 리프레시 토큰 없이 로그아웃 시도")
logger.warn("[로그아웃 요청] 리프레시 토큰 없음 $requestInfo")
}
cookieUtils.deleteRefreshTokenCookie(response)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uoslife.servermeeting.global.common.dto

data class RequestInfoDto(val ip: String, val userAgent: String) {
override fun toString(): String {
return "ip: $ip, userAgent: $userAgent"
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/uoslife/servermeeting/global/util/RequestUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package uoslife.servermeeting.global.util

import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Component
import uoslife.servermeeting.global.common.dto.RequestInfoDto

@Component
class RequestUtils {
companion object {
private const val X_FORWARDED_FOR = "X-Forwarded-For"
private const val PROXY_CLIENT_IP = "Proxy-Client-IP"
private const val WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP"
}

fun toRequestInfoDto(request: HttpServletRequest) =
RequestInfoDto(ip = getClientIp(request), userAgent = getUserAgent(request))

fun getClientIp(request: HttpServletRequest): String =
when {
request.getHeader(X_FORWARDED_FOR) != null ->
request.getHeader(X_FORWARDED_FOR).split(",")[0]
request.getHeader(PROXY_CLIENT_IP) != null -> request.getHeader(PROXY_CLIENT_IP)
request.getHeader(WL_PROXY_CLIENT_IP) != null -> request.getHeader(WL_PROXY_CLIENT_IP)
else -> request.remoteAddr
}

fun getUserAgent(request: HttpServletRequest): String {
return request.getHeader("User-Agent") ?: "Unknown"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class UserService(

deleteRefreshInfo(user, response)
userRepository.delete(user)
logger.info("[유저 삭제 완료] User Email : $deletedEmail")
logger.info("[유저 삭제] email: $deletedEmail")
}

private fun deleteRefreshInfo(user: User, response: HttpServletResponse) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import uoslife.servermeeting.global.auth.dto.response.JwtResponse
import uoslife.servermeeting.global.auth.service.AuthService
import uoslife.servermeeting.global.error.ErrorResponse
import uoslife.servermeeting.global.util.RequestUtils
import uoslife.servermeeting.user.service.UserService
import uoslife.servermeeting.verification.dto.request.VerifyEmailRequest
import uoslife.servermeeting.verification.dto.response.SendVerificationEmailResponse
Expand All @@ -26,6 +28,7 @@ class VerificationApi(
private val emailVerificationService: EmailVerificationService,
private val userService: UserService,
private val authService: AuthService,
private val requestUtils: RequestUtils,
) {
@Operation(summary = "인증메일 전송", description = "이메일로 인증코드를 전송합니다.")
@ApiResponses(
Expand Down Expand Up @@ -80,9 +83,11 @@ class VerificationApi(
)
@PostMapping("/send-email")
fun sendVerificationEmail(
@RequestParam email: String
@RequestParam email: String,
request: HttpServletRequest
): ResponseEntity<SendVerificationEmailResponse> {
val response = emailVerificationService.sendVerificationEmail(email)
val requestInfo = requestUtils.toRequestInfoDto(request)
val response = emailVerificationService.sendVerificationEmail(email, requestInfo)
return ResponseEntity.ok(response)
}

Expand Down Expand Up @@ -180,16 +185,19 @@ class VerificationApi(
// TODO: Service layer로 이동해야함
@PostMapping("/verify-email")
fun verifyEmail(
@Valid @RequestBody request: VerifyEmailRequest,
@Valid @RequestBody body: VerifyEmailRequest,
@PathVariable userId: Long,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<JwtResponse> {
emailVerificationService.verifyEmail(request.email, request.code)
val requestInfo = requestUtils.toRequestInfoDto(request)
emailVerificationService.verifyEmail(body.email, body.code, requestInfo)

val user =
try {
userService.getUserByEmail(request.email)
userService.getUserByEmail(body.email)
} catch (e: Exception) {
userService.createUserByEmail(request.email)
userService.createUserByEmail(body.email)
}

val accessToken = authService.issueTokens(user.id!!, response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import uoslife.servermeeting.global.common.dto.RequestInfoDto
import uoslife.servermeeting.verification.dto.response.SendVerificationEmailResponse
import uoslife.servermeeting.verification.exception.*
import uoslife.servermeeting.verification.util.VerificationConstants
Expand All @@ -24,7 +25,10 @@ class EmailVerificationService(
private val logger = LoggerFactory.getLogger(EmailVerificationService::class.java)
}

fun sendVerificationEmail(email: String): SendVerificationEmailResponse {
fun sendVerificationEmail(
email: String,
requestInfo: RequestInfoDto
): SendVerificationEmailResponse {
// 이메일 형식 검증
validateEmail(email)
// 발송 제한 확인
Expand All @@ -33,9 +37,9 @@ class EmailVerificationService(
val asyncResult = asyncEmailService.sendEmailAsync(email)
asyncResult.whenComplete { _, exception ->
if (exception != null) {
logger.warn("[이메일 전송 실패] EMAIL : $email")
logger.warn("[이메일 전송 실패] email: $email, $requestInfo")
} else {
logger.info("[이메일 전송 성공] EMAIL : $email")
logger.info("[이메일 전송 성공] email: $email, $requestInfo")
}
}
// 코드 만료 시각 계산
Expand All @@ -47,13 +51,13 @@ class EmailVerificationService(
)
}

fun verifyEmail(email: String, code: String) {
fun verifyEmail(email: String, code: String, requestInfo: RequestInfoDto) {
validateVerificationAttempts(email)
incrementVerificationAttempts(email)
// Redis에서 인증 코드 조회
val redisCode = getVerificationCode(email)
// 인증 코드 검증
validateVerificationCode(redisCode, code)
validateVerificationCode(redisCode, code, email, requestInfo)
// 검증 성공한 코드 삭제
clearVerificationData(email)
}
Expand All @@ -64,10 +68,17 @@ class EmailVerificationService(
return redisTemplate.opsForValue().get(verificationCodeKey).toString()
}

private fun validateVerificationCode(redisCode: String, code: String) {
private fun validateVerificationCode(
redisCode: String,
code: String,
email: String,
requestInfo: RequestInfoDto
) {
if (redisCode != code) {
logger.warn("[이메일 인증 실패] email: $email, $requestInfo")
throw EmailVerificationCodeMismatchException()
}
logger.info("[이메일 인증 성공] email: $email")
}

private fun validateSendCount(email: String) {
Expand Down

0 comments on commit 5b04279

Please sign in to comment.