Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/275] api 요청 로그 filter 구현 및 로그인 요청 로그 출력 추가 #276

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 11 additions & 64 deletions src/main/java/com/gamegoo/aop/LogAspect.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.gamegoo.aop;

import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.jboss.logging.MDC;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
Expand All @@ -20,63 +18,31 @@
@Component
public class LogAspect {

// @Pointcut("execution(* com.gamegoo.controller..*.*(..))")
// public void all() {
//
// }

@Pointcut("execution(* com.gamegoo.controller..*.*(..))")
public void controller() {
}

// @Around("all()")
// public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
// long start = System.currentTimeMillis();
// try {
// Object result = joinPoint.proceed();
// return result;
// } finally {
// long end = System.currentTimeMillis();
// long timeinMs = end - start;
// log.info("{} | time = {}ms", joinPoint.getSignature(), timeinMs);
// }
// }

@Around("controller()")
public Object loggingBefore(ProceedingJoinPoint joinPoint) throws Throwable {
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

String controllerName = joinPoint.getSignature().getDeclaringType().getName();
String methodName = joinPoint.getSignature().getName();
Map<String, Object> params = new HashMap<>();

try {
String decodedURI = URLDecoder.decode(request.getRequestURI(), "UTF-8");
String clientIp = getClientIp(request); // IP 주소 가져오기

params.put("controller", controllerName);
params.put("method", methodName);
params.put("params", getParams(request));
params.put("request_uri", decodedURI);
params.put("http_method", request.getMethod());
params.put("client_ip", clientIp); // IP 주소 추가

} catch (Exception e) {
log.error("LoggerAspect error", e);
// requestId가 없을 경우 기본값 설정
String requestId = (String) MDC.get("requestId");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 혹시 이 requestId는 무엇인가요..? 이 메소드가 무슨 일을 하는지 궁금한데 알려주실 수 있으신가요..?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한번에 여러 요청이 들어오면 로그가 req, res 한 쌍으로 연속해서 출력되지 않고 req req req res res res 이런식으로 출력되는데,, MDC를 사용하면 매 요청별로 고유의 아이디 값을 저장할 수 있다고 하더라구요..! 요청 - 응답 쌍을 더 구분하기 쉽게 하고 싶어서 추가했습니다!

if (requestId == null || requestId.isEmpty()) {
requestId = "N/A"; // requestId가 없을 경우 기본값을 설정
}

log.info("<REQUEST> [{}] {} | IP: {} | Controller: {} | Method: {} | Params: {}",
params.get("http_method"), params.get("request_uri"), params.get("client_ip"),
controllerName, methodName, params.get("params"));
String controllerName = joinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = joinPoint.getSignature().getName();

long start = System.currentTimeMillis();
Object result = joinPoint.proceed();

long executionTime = System.currentTimeMillis() - start;

log.info("<RESPONSE> [{}] {} | IP: {} | Controller: {} | Method: {} | Execution Time: {}ms",
params.get("http_method"), params.get("request_uri"), params.get("client_ip"),
controllerName, methodName, executionTime);
log.info("[requestId: {}] Method: {}.{} | Prams: {} | Execution Time: {}ms",
requestId,
controllerName, methodName, getParams(request), executionTime);

return result;
}
Expand All @@ -92,23 +58,4 @@ private static JSONObject getParams(HttpServletRequest request) {
return jsonObject;
}

private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
// X-Forwarded-For 헤더에 여러 IP가 포함될 경우 첫 번째 IP가 실제 클라이언트 IP
return ip.split(",")[0].trim();
}

// X-Forwarded-For 헤더가 없거나 유효하지 않은 경우 다른 헤더를 확인
ip = request.getHeader("Proxy-Client-IP");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}

return ip;
}

}
4 changes: 3 additions & 1 deletion src/main/java/com/gamegoo/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import static org.springframework.security.config.Customizer.withDefaults;

import com.gamegoo.apiPayload.exception.handler.JWTExceptionHandlerFilter;
import com.gamegoo.filter.JWTExceptionHandlerFilter;
import com.gamegoo.filter.JWTFilter;
import com.gamegoo.filter.LoggingFilter;
import com.gamegoo.filter.LoginFilter;
import com.gamegoo.repository.member.MemberRepository;
import com.gamegoo.security.CustomUserDetailService;
Expand Down Expand Up @@ -74,6 +75,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.anyRequest().authenticated())
.addFilterBefore(new JWTExceptionHandlerFilter(),
UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new LoggingFilter(jwtUtil), JWTExceptionHandlerFilter.class)
.addFilterAt(
new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil,
memberRepository), UsernamePasswordAuthenticationFilter.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,77 @@
package com.gamegoo.apiPayload.exception.handler;
package com.gamegoo.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gamegoo.apiPayload.ApiResponse;
import com.gamegoo.apiPayload.code.status.ErrorStatus;
import io.jsonwebtoken.JwtException;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
public class JWTExceptionHandlerFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String requestId = UUID.randomUUID().toString(); // 고유한 requestId 생성
String requestUrl = request.getRequestURI();
String httpMethod = request.getMethod(); // HTTP 메소드 추출
String clientIp = getClientIp(request);
String memberId = "Unauthenticated"; // 기본값으로 설정 (로그인 상태가 아닐 때)
try {
filterChain.doFilter(request, response);
} catch (JwtException e) {

if (Objects.equals(e.getMessage(), "Token expired")) {
setErrorResponse(response, ErrorStatus.TOKEN_EXPIRED);
setErrorResponse(response, ErrorStatus.TOKEN_EXPIRED, requestId, httpMethod,
requestUrl, clientIp, memberId);
} else if (Objects.equals(e.getMessage(), "Token null")) {
setErrorResponse(response, ErrorStatus.TOKEN_NULL);
setErrorResponse(response, ErrorStatus.TOKEN_NULL, requestId, httpMethod,
requestUrl, clientIp, memberId);
} else if (Objects.equals(e.getMessage(), "No Member")) {
setErrorResponse(response, ErrorStatus.MEMBER_NOT_FOUND);
setErrorResponse(response, ErrorStatus.MEMBER_NOT_FOUND, requestId, httpMethod,
requestUrl, clientIp, memberId);
} else {
setErrorResponse(response, ErrorStatus.INVALID_TOKEN);
setErrorResponse(response, ErrorStatus.INVALID_TOKEN, requestId, httpMethod,
requestUrl, clientIp, memberId);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void setErrorResponse(HttpServletResponse response, ErrorStatus errorStatus) throws IOException {
private void setErrorResponse(HttpServletResponse response, ErrorStatus errorStatus,
String requestId, String httpMethod, String requestUrl, String clientIp, String memberId)
throws IOException {
// 에러 응답 생성하기
ApiResponse<Object> apiResponse = ApiResponse.onFailure(errorStatus.getCode(), errorStatus.getMessage(), null);
ApiResponse<Object> apiResponse = ApiResponse.onFailure(errorStatus.getCode(),
errorStatus.getMessage(), null);
response.setStatus(errorStatus.getHttpStatus().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

log.info("[requestId: {}] [{}] {} | IP: {} | Member ID: {} | Status: {}", requestId,
httpMethod, requestUrl, clientIp, memberId,
errorStatus.getHttpStatus().value() + " " + errorStatus.getMessage());

new ObjectMapper().writeValue(response.getWriter(), apiResponse);
}

// 클라이언트 IP 가져오는 메소드
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

125 changes: 125 additions & 0 deletions src/main/java/com/gamegoo/filter/LoggingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.gamegoo.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gamegoo.util.JWTUtil;
import io.jsonwebtoken.JwtException;
import java.io.IOException;
import java.util.UUID;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jboss.logging.MDC;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
public class LoggingFilter extends OncePerRequestFilter {

private final JWTUtil jwtUtil;

public LoggingFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// /v1/member/login 경로에 대해서는 필터 pass
String requestUrl = request.getRequestURI();
if ("/v1/member/login".equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}

String requestId = UUID.randomUUID().toString(); // 고유한 requestId 생성

try {
MDC.put("requestId", requestId);

// 요청 정보 추출
String httpMethod = request.getMethod(); // HTTP 메소드 추출
String clientIp = getClientIp(request);
String jwtToken = extractJwtToken(request);
String memberId = null;
boolean jwtTokenPresent = jwtToken != null;
String params = getParamsAsString(request);

// 토큰이 있을 경우 사용자 ID 추출
if (jwtTokenPresent) {
try {
memberId = jwtUtil.getId(jwtToken).toString();
} catch (JwtException e) {
log.error("JWT Exception: {}", e.getMessage());
memberId = "Invalid JWT"; // JWT 에러가 있을 경우
}
} else {
memberId = "Unauthenticated"; // 비로그인 사용자
}

// 요청 로그 기록
if (params != null && !params.isEmpty() && !params.equals("{}")) {
log.info("[requestId: {}] [{}] {} | IP: {} | Member ID: {} | Params: {}", requestId,
httpMethod, requestUrl, clientIp, memberId, params);
} else {
log.info("[requestId: {}] [{}] {} | IP: {} | Member ID: {}", requestId,
httpMethod, requestUrl, clientIp, memberId);
}

// 실행 시간 측정을 위한 시작 시간
// long startTime = System.currentTimeMillis();

// 요청 처리
filterChain.doFilter(request, response);

// 응답 정보 추출
// long executionTime = System.currentTimeMillis() - startTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지우지 않고 주석으로 남겨둔 이유가 궁금합니당..!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 혹시 나중에 시간 정보 출력이 필요할까봐 놔두긴 했는데,, 그냥 지우는게 나을 것 같아요 ! 수정하고 머지하겠습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 아니 수정하고 머지하겠다고 해놓고 바로 머지를 눌러버렸어요....... 다음 pr에서 수정하겠습니다 ㅠㅠ

int statusCode = response.getStatus();
String statusMessage = getStatusMessage(statusCode);

// 응답 로그 기록
log.info("[requestId: {}] [{}] {} | IP: {} | Member ID: {} | Status: {}", requestId,
httpMethod, requestUrl,
clientIp, memberId, statusMessage);
} finally {
MDC.remove("requestId");
}
}

// JWT 토큰을 Authorization 헤더에서 추출하는 메서드
private String extractJwtToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
return authorizationHeader.substring(7);
}
return null;
}

// 클라이언트 IP 가져오는 메소드
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0].trim();
}
return request.getRemoteAddr();
}

// 상태 코드에 맞는 메시지 반환
private String getStatusMessage(int statusCode) {
HttpStatus httpStatus = HttpStatus.resolve(statusCode);
return httpStatus != null ? statusCode + " " + httpStatus.getReasonPhrase()
: String.valueOf(statusCode);
}

private String getParamsAsString(HttpServletRequest request) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(request.getParameterMap());
} catch (JsonProcessingException e) {
return "Unable to parse parameters";
}
}
}
Loading
Loading