-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
} | ||
} | ||
|
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지우지 않고 주석으로 남겨둔 이유가 궁금합니당..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앗 혹시 나중에 시간 정보 출력이 필요할까봐 놔두긴 했는데,, 그냥 지우는게 나을 것 같아요 ! 수정하고 머지하겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 혹시 이 requestId는 무엇인가요..? 이 메소드가 무슨 일을 하는지 궁금한데 알려주실 수 있으신가요..?
There was a problem hiding this comment.
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를 사용하면 매 요청별로 고유의 아이디 값을 저장할 수 있다고 하더라구요..! 요청 - 응답 쌍을 더 구분하기 쉽게 하고 싶어서 추가했습니다!