init: 클린 기반 auth 서버 설계
This commit is contained in:
+1
File diff suppressed because one or more lines are too long
+115
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>ApiErrorController.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">ApiErrorController.java</span></div><h1>ApiErrorController.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.application.support.exception.ClientFacingErrorCode;
|
||||
import com.project.auth.application.support.exception.CommonErrorCode;
|
||||
import com.project.auth.application.support.logging.LogSanitizer;
|
||||
import com.project.auth.presentation.support.exception.PresentationErrorCode;
|
||||
import com.project.auth.presentation.support.response.ApiResult;
|
||||
import com.project.auth.presentation.support.response.ApiResultFactory;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.webmvc.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code @RestControllerAdvice}를 우회하는 요청(필터에서 던져진 예외,
|
||||
* {@code response.sendError(...)} 호출, 컨테이너 레벨 라우팅 실패, 다른 핸들러 내부의
|
||||
* 이중 폴트 등)에 대한 최후의 에러 렌더러.
|
||||
*
|
||||
* 컨테이너가 결정한 상태 코드는 그대로 보존하고, 본문만 {@link ApiResult} 형태로 정규화한다.
|
||||
* 5xx는 내부 분류 노출을 막기 위해 {@code COMMON-999}로 정규화하고, 4xx는
|
||||
* {@link PresentationErrorCode} 기반 코드로 매핑하여 클라이언트 SDK가
|
||||
* "요청이 잘못됐다"와 "서버가 깨졌다"를 구분할 수 있게 한다.
|
||||
*
|
||||
* 의존성 정책: 이 컨트롤러는 Spring Boot의 ErrorAttributes를 사용하지 않는다.
|
||||
* 필요한 정보(상태 코드, 원인 예외, 원래 요청 경로)는 모두 서블릿 표준
|
||||
* RequestDispatcher.ERROR_* attribute로부터 직접 추출한다. 이렇게 두면
|
||||
* ErrorAttributeOptions의 변경(예: 스택트레이스 포함)이 이 컨트롤러가 노출하는 정보를
|
||||
* 본의 아니게 확장하지 못하며, 빈 의존 그래프도 작아진다.
|
||||
*/
|
||||
@RestController
|
||||
class ApiErrorController implements ErrorController {
|
||||
|
||||
<span class="fc" id="L40"> private static final Logger log = LoggerFactory.getLogger(ApiErrorController.class);</span>
|
||||
|
||||
private final ApiResultFactory apiResultFactory;
|
||||
|
||||
<span class="fc" id="L44"> ApiErrorController(ApiResultFactory apiResultFactory) {</span>
|
||||
<span class="fc" id="L45"> this.apiResultFactory = Objects.requireNonNull(apiResultFactory, "apiResultFactory must not be null");</span>
|
||||
<span class="fc" id="L46"> }</span>
|
||||
|
||||
@RequestMapping("/error")
|
||||
ResponseEntity<ApiResult<Void>> error(HttpServletRequest request) {
|
||||
<span class="fc" id="L50"> int rawStatus = resolveStatusCode(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));</span>
|
||||
<span class="fc" id="L51"> HttpStatus httpStatus = safeStatus(rawStatus);</span>
|
||||
<span class="fc" id="L52"> Throwable error = resolveError(request);</span>
|
||||
<span class="fc" id="L53"> String requestPath = LogSanitizer.requestPath(originalRequestPath(request));</span>
|
||||
<span class="fc" id="L54"> String method = request.getMethod();</span>
|
||||
|
||||
<span class="pc bpc" id="L56" title="1 of 4 branches missed."> if (httpStatus.is5xxServerError() && error != null) {</span>
|
||||
<span class="nc" id="L57"> log.error("Unhandled exception. method={} requestPath={} status={}",</span>
|
||||
<span class="nc" id="L58"> method, requestPath, httpStatus.value(), error);</span>
|
||||
<span class="fc bfc" id="L59" title="All 2 branches covered."> } else if (httpStatus.is5xxServerError()) {</span>
|
||||
<span class="fc" id="L60"> log.error("Unhandled error response. method={} requestPath={} status={}",</span>
|
||||
<span class="fc" id="L61"> method, requestPath, httpStatus.value());</span>
|
||||
} else {
|
||||
<span class="fc" id="L63"> log.warn("Unhandled error response. method={} requestPath={} status={}",</span>
|
||||
<span class="fc" id="L64"> method, requestPath, httpStatus.value());</span>
|
||||
}
|
||||
|
||||
<span class="fc" id="L67"> ClientFacingErrorCode errorCode = classify(httpStatus);</span>
|
||||
|
||||
<span class="fc" id="L69"> return ResponseEntity.status(httpStatus)</span>
|
||||
<span class="fc" id="L70"> .body(apiResultFactory.failure(errorCode.code(), errorCode.message()));</span>
|
||||
}
|
||||
|
||||
private static ClientFacingErrorCode classify(HttpStatus status) {
|
||||
<span class="fc bfc" id="L74" title="All 2 branches covered."> if (status.is5xxServerError()) {</span>
|
||||
<span class="fc" id="L75"> return CommonErrorCode.INTERNAL_SERVER_ERROR;</span>
|
||||
}
|
||||
<span class="pc bpc" id="L77" title="3 of 6 branches missed."> return switch (status) {</span>
|
||||
<span class="fc" id="L78"> case NOT_FOUND -> PresentationErrorCode.RESOURCE_NOT_FOUND;</span>
|
||||
<span class="fc" id="L79"> case METHOD_NOT_ALLOWED -> PresentationErrorCode.METHOD_NOT_ALLOWED;</span>
|
||||
<span class="nc" id="L80"> case UNSUPPORTED_MEDIA_TYPE -> PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE;</span>
|
||||
<span class="nc" id="L81"> case NOT_ACCEPTABLE -> PresentationErrorCode.NOT_ACCEPTABLE;</span>
|
||||
<span class="nc" id="L82"> case CONTENT_TOO_LARGE -> PresentationErrorCode.PAYLOAD_TOO_LARGE;</span>
|
||||
<span class="fc" id="L83"> default -> PresentationErrorCode.UNHANDLED_CLIENT_ERROR;</span>
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpStatus safeStatus(int rawStatus) {
|
||||
try {
|
||||
<span class="fc" id="L89"> return HttpStatus.valueOf(rawStatus);</span>
|
||||
<span class="fc" id="L90"> } catch (IllegalArgumentException ignored) {</span>
|
||||
<span class="fc" id="L91"> return HttpStatus.INTERNAL_SERVER_ERROR;</span>
|
||||
}
|
||||
}
|
||||
|
||||
private static int resolveStatusCode(Object status) {
|
||||
<span class="fc bfc" id="L96" title="All 2 branches covered."> if (status instanceof Integer value) {</span>
|
||||
<span class="fc" id="L97"> return value;</span>
|
||||
}
|
||||
<span class="fc" id="L99"> return HttpStatus.INTERNAL_SERVER_ERROR.value();</span>
|
||||
}
|
||||
|
||||
private static String originalRequestPath(HttpServletRequest request) {
|
||||
<span class="fc" id="L103"> Object path = request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);</span>
|
||||
<span class="pc bpc" id="L104" title="1 of 4 branches missed."> if (path instanceof String requestUri && !requestUri.isBlank()) {</span>
|
||||
<span class="fc" id="L105"> return requestUri;</span>
|
||||
}
|
||||
<span class="fc" id="L107"> return request.getRequestURI();</span>
|
||||
}
|
||||
|
||||
private static Throwable resolveError(HttpServletRequest request) {
|
||||
<span class="fc" id="L111"> Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);</span>
|
||||
<span class="pc bpc" id="L112" title="1 of 2 branches missed."> return exception instanceof Throwable throwable ? throwable : null;</span>
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>InfrastructureExceptionHandler</title><script type="text/javascript" src="../jacoco-resources/sort.js"></script></head><body onload="initialSort(['breadcrumb'])"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.html" class="el_package">com.project.auth.config.web</a> > <span class="el_class">InfrastructureExceptionHandler</span></div><h1>InfrastructureExceptionHandler</h1><table class="coverage" cellspacing="0" id="coveragetable"><thead><tr><td class="sortable" id="a" onclick="toggleSort(this)">Element</td><td class="down sortable bar" id="b" onclick="toggleSort(this)">Missed Instructions</td><td class="sortable ctr2" id="c" onclick="toggleSort(this)">Cov.</td><td class="sortable bar" id="d" onclick="toggleSort(this)">Missed Branches</td><td class="sortable ctr2" id="e" onclick="toggleSort(this)">Cov.</td><td class="sortable ctr1" id="f" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="g" onclick="toggleSort(this)">Cxty</td><td class="sortable ctr1" id="h" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="i" onclick="toggleSort(this)">Lines</td><td class="sortable ctr1" id="j" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="k" onclick="toggleSort(this)">Methods</td></tr></thead><tfoot><tr><td>Total</td><td class="bar">0 of 80</td><td class="ctr2">100%</td><td class="bar">0 of 0</td><td class="ctr2">n/a</td><td class="ctr1">0</td><td class="ctr2">4</td><td class="ctr1">0</td><td class="ctr2">19</td><td class="ctr1">0</td><td class="ctr2">4</td></tr></tfoot><tbody><tr><td id="a0"><a href="InfrastructureExceptionHandler.java.html#L42" class="el_method">handleInfrastructureException(InfrastructureException, HttpServletRequest)</a></td><td class="bar" id="b0"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="38" alt="38"/></td><td class="ctr2" id="c0">100%</td><td class="bar" id="d0"/><td class="ctr2" id="e0">n/a</td><td class="ctr1" id="f0">0</td><td class="ctr2" id="g0">1</td><td class="ctr1" id="h0">0</td><td class="ctr2" id="i0">8</td><td class="ctr1" id="j0">0</td><td class="ctr2" id="k0">1</td></tr><tr><td id="a1"><a href="InfrastructureExceptionHandler.java.html#L62" class="el_method">handleLeakedDomainException(DomainException, HttpServletRequest)</a></td><td class="bar" id="b1"><img src="../jacoco-resources/greenbar.gif" width="101" height="10" title="32" alt="32"/></td><td class="ctr2" id="c1">100%</td><td class="bar" id="d1"/><td class="ctr2" id="e1">n/a</td><td class="ctr1" id="f1">0</td><td class="ctr2" id="g1">1</td><td class="ctr1" id="h1">0</td><td class="ctr2" id="i1">7</td><td class="ctr1" id="j1">0</td><td class="ctr2" id="k1">1</td></tr><tr><td id="a2"><a href="InfrastructureExceptionHandler.java.html#L33" class="el_method">InfrastructureExceptionHandler(ApiResultFactory)</a></td><td class="bar" id="b2"><img src="../jacoco-resources/greenbar.gif" width="18" height="10" title="6" alt="6"/></td><td class="ctr2" id="c2">100%</td><td class="bar" id="d2"/><td class="ctr2" id="e2">n/a</td><td class="ctr1" id="f2">0</td><td class="ctr2" id="g2">1</td><td class="ctr1" id="h2">0</td><td class="ctr2" id="i2">3</td><td class="ctr1" id="j2">0</td><td class="ctr2" id="k2">1</td></tr><tr><td id="a3"><a href="InfrastructureExceptionHandler.java.html#L29" class="el_method">static {...}</a></td><td class="bar" id="b3"><img src="../jacoco-resources/greenbar.gif" width="12" height="10" title="4" alt="4"/></td><td class="ctr2" id="c3">100%</td><td class="bar" id="d3"/><td class="ctr2" id="e3">n/a</td><td class="ctr1" id="f3">0</td><td class="ctr2" id="g3">1</td><td class="ctr1" id="h3">0</td><td class="ctr2" id="i3">1</td><td class="ctr1" id="j3">0</td><td class="ctr2" id="k3">1</td></tr></tbody></table><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>InfrastructureExceptionHandler.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">InfrastructureExceptionHandler.java</span></div><h1>InfrastructureExceptionHandler.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.application.support.exception.CommonErrorCode;
|
||||
import com.project.auth.application.support.logging.LogSanitizer;
|
||||
import com.project.auth.domain.user.exception.DomainException;
|
||||
import com.project.auth.infrastructure.support.exception.InfrastructureException;
|
||||
import com.project.auth.presentation.support.exception.ApiErrorHttpStatusMapper;
|
||||
import com.project.auth.presentation.support.response.ApiResult;
|
||||
import com.project.auth.presentation.support.response.ApiResultFactory;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* InfrastructureErrorCode is kept as an internal classification for logs and alerting.
|
||||
* Client responses are intentionally normalized to COMMON-999 to avoid exposing internal dependency details.
|
||||
* This advice stays in bootstrap because moving it to presentation would create a
|
||||
* presentation -> infrastructure dependency and break the layer rule.
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class InfrastructureExceptionHandler {
|
||||
|
||||
<span class="fc" id="L29"> private static final Logger log = LoggerFactory.getLogger(InfrastructureExceptionHandler.class);</span>
|
||||
|
||||
private final ApiResultFactory apiResultFactory;
|
||||
|
||||
<span class="fc" id="L33"> public InfrastructureExceptionHandler(ApiResultFactory apiResultFactory) {</span>
|
||||
<span class="fc" id="L34"> this.apiResultFactory = apiResultFactory;</span>
|
||||
<span class="fc" id="L35"> }</span>
|
||||
|
||||
@ExceptionHandler(InfrastructureException.class)
|
||||
public ResponseEntity<ApiResult<Void>> handleInfrastructureException(
|
||||
InfrastructureException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
<span class="fc" id="L42"> log.error(</span>
|
||||
"Infrastructure failure. errorCode={} method={} requestPath={}",
|
||||
<span class="fc" id="L44"> exception.getErrorCode().code(),</span>
|
||||
<span class="fc" id="L45"> request.getMethod(),</span>
|
||||
<span class="fc" id="L46"> LogSanitizer.requestPath(request.getRequestURI()),</span>
|
||||
exception
|
||||
);
|
||||
|
||||
<span class="fc" id="L50"> return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR))</span>
|
||||
<span class="fc" id="L51"> .body(apiResultFactory.failure(</span>
|
||||
<span class="fc" id="L52"> CommonErrorCode.INTERNAL_SERVER_ERROR.code(),</span>
|
||||
<span class="fc" id="L53"> CommonErrorCode.INTERNAL_SERVER_ERROR.message()</span>
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(DomainException.class)
|
||||
public ResponseEntity<ApiResult<Void>> handleLeakedDomainException(
|
||||
DomainException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
<span class="fc" id="L62"> log.error(</span>
|
||||
"Leaked domain exception. method={} requestPath={}",
|
||||
<span class="fc" id="L64"> request.getMethod(),</span>
|
||||
<span class="fc" id="L65"> LogSanitizer.requestPath(request.getRequestURI()),</span>
|
||||
exception
|
||||
);
|
||||
|
||||
<span class="fc" id="L69"> return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR))</span>
|
||||
<span class="fc" id="L70"> .body(apiResultFactory.failure(</span>
|
||||
<span class="fc" id="L71"> CommonErrorCode.INTERNAL_SERVER_ERROR.code(),</span>
|
||||
<span class="fc" id="L72"> CommonErrorCode.INTERNAL_SERVER_ERROR.message()</span>
|
||||
));
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+1
File diff suppressed because one or more lines are too long
+90
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>RequestAccessLogFilter.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">RequestAccessLogFilter.java</span></div><h1>RequestAccessLogFilter.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.application.support.logging.LogSanitizer;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class RequestAccessLogFilter extends OncePerRequestFilter {
|
||||
|
||||
<span class="fc" id="L19"> private static final Logger log = LoggerFactory.getLogger("http.access");</span>
|
||||
private static final String ACCESS_EVENT_TYPE = "HTTP_ACCESS";
|
||||
|
||||
private final List<String> excludedPathPrefixes;
|
||||
|
||||
<span class="fc" id="L24"> public RequestAccessLogFilter(AccessLogProperties accessLogProperties) {</span>
|
||||
<span class="fc bfc" id="L25" title="All 2 branches covered."> this.excludedPathPrefixes = accessLogProperties.excludedPathPrefixes() == null</span>
|
||||
<span class="fc" id="L26"> ? List.of()</span>
|
||||
<span class="fc" id="L27"> : List.copyOf(accessLogProperties.excludedPathPrefixes());</span>
|
||||
<span class="fc" id="L28"> }</span>
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
<span class="fc" id="L32"> String path = request.getRequestURI();</span>
|
||||
<span class="fc" id="L33"> return excludedPathPrefixes.stream().anyMatch(path::startsWith);</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterAsyncDispatch() {
|
||||
<span class="nc" id="L38"> return true;</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterErrorDispatch() {
|
||||
<span class="nc" id="L43"> return true;</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
<span class="fc" id="L51"> long startTime = System.nanoTime();</span>
|
||||
|
||||
try {
|
||||
<span class="fc" id="L54"> filterChain.doFilter(request, response);</span>
|
||||
} finally {
|
||||
<span class="fc" id="L56"> long durationMs = (System.nanoTime() - startTime) / 1_000_000;</span>
|
||||
<span class="fc" id="L57"> logRequestSummary(request, response, durationMs);</span>
|
||||
}
|
||||
<span class="fc" id="L59"> }</span>
|
||||
|
||||
private void logRequestSummary(HttpServletRequest request, HttpServletResponse response, long durationMs) {
|
||||
<span class="fc" id="L62"> String method = request.getMethod();</span>
|
||||
<span class="fc" id="L63"> String path = LogSanitizer.requestPath(request.getRequestURI());</span>
|
||||
<span class="fc" id="L64"> int status = response.getStatus();</span>
|
||||
<span class="fc" id="L65"> String remoteIp = LogSanitizer.clientIp(request.getRemoteAddr());</span>
|
||||
<span class="fc" id="L66"> String actorId = resolveActorId();</span>
|
||||
<span class="pc bpc" id="L67" title="1 of 2 branches missed."> String result = status < 400 ? "success" : "failure";</span>
|
||||
|
||||
<span class="fc" id="L69"> log.atInfo()</span>
|
||||
<span class="fc" id="L70"> .addKeyValue("eventType", ACCESS_EVENT_TYPE)</span>
|
||||
<span class="fc" id="L71"> .addKeyValue("method", method)</span>
|
||||
<span class="fc" id="L72"> .addKeyValue("requestPath", path)</span>
|
||||
<span class="fc" id="L73"> .addKeyValue("status", status)</span>
|
||||
<span class="fc" id="L74"> .addKeyValue("durationMs", durationMs)</span>
|
||||
<span class="fc" id="L75"> .addKeyValue("remoteIp", remoteIp)</span>
|
||||
<span class="fc" id="L76"> .addKeyValue("actorId", actorId)</span>
|
||||
<span class="fc" id="L77"> .addKeyValue("result", result)</span>
|
||||
<span class="fc" id="L78"> .log("ACCESS");</span>
|
||||
<span class="fc" id="L79"> }</span>
|
||||
|
||||
private String resolveActorId() {
|
||||
<span class="fc" id="L82"> Authentication authentication = SecurityContextHolder.getContext().getAuthentication();</span>
|
||||
<span class="pc bpc" id="L83" title="1 of 4 branches missed."> if (authentication != null && authentication.isAuthenticated()</span>
|
||||
<span class="pc bpc" id="L84" title="1 of 2 branches missed."> && !"anonymousUser".equals(authentication.getPrincipal())) {</span>
|
||||
<span class="fc" id="L85"> return LogSanitizer.actorId(authentication.getName());</span>
|
||||
}
|
||||
<span class="fc" id="L87"> return LogSanitizer.actorId("anonymous");</span>
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+1
File diff suppressed because one or more lines are too long
+69
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>RequestBoundApiResultFactory.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">RequestBoundApiResultFactory.java</span></div><h1>RequestBoundApiResultFactory.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.presentation.support.response.ApiResult;
|
||||
import com.project.auth.presentation.support.response.ApiResultFactory;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class RequestBoundApiResultFactory implements ApiResultFactory {
|
||||
|
||||
private static final String TRACE_ID_KEY = "traceId";
|
||||
/**
|
||||
* MDC에 traceId가 없을 때 사용하는 sentinel. JSON {@code null}로 그대로 두면
|
||||
* TraceIdFilter가 누락/오설정된 사실이 가려지므로, 명시적인 placeholder 문자열로
|
||||
* 로그·대시보드에서 즉시 식별 가능하게 한다.
|
||||
*/
|
||||
static final String MISSING_TRACE_ID = "-";
|
||||
<span class="fc" id="L22"> private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_INSTANT;</span>
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
<span class="fc" id="L26"> public RequestBoundApiResultFactory(Clock clock) {</span>
|
||||
<span class="fc" id="L27"> this.clock = Objects.requireNonNull(clock, "clock must not be null");</span>
|
||||
<span class="fc" id="L28"> }</span>
|
||||
|
||||
@Override
|
||||
public <T> ApiResult<T> success(String code, String message, T data) {
|
||||
<span class="fc" id="L32"> return result(true, code, message, data, null);</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResult<Void> success(String code, String message) {
|
||||
<span class="nc" id="L37"> return result(true, code, message, null, null);</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResult<Void> failure(String code, String message) {
|
||||
<span class="fc" id="L42"> return result(false, code, message, null, null);</span>
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResult<Void> failure(String code, String message, Map<String, List<String>> errors) {
|
||||
<span class="fc" id="L47"> return result(false, code, message, null, errors);</span>
|
||||
}
|
||||
|
||||
private <T> ApiResult<T> result(
|
||||
boolean success,
|
||||
String code,
|
||||
String message,
|
||||
T data,
|
||||
Map<String, List<String>> errors
|
||||
) {
|
||||
<span class="fc" id="L57"> String traceId = MDC.get(TRACE_ID_KEY);</span>
|
||||
<span class="fc" id="L58"> return new ApiResult<>(</span>
|
||||
success,
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
errors,
|
||||
<span class="fc bfc" id="L64" title="All 4 branches covered."> traceId == null || traceId.isBlank() ? MISSING_TRACE_ID : traceId,</span>
|
||||
<span class="fc" id="L65"> TIMESTAMP_FORMATTER.format(clock.instant())</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+1
File diff suppressed because one or more lines are too long
+144
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>SecurityResponseExceptionHandler.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">SecurityResponseExceptionHandler.java</span></div><h1>SecurityResponseExceptionHandler.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.application.support.audit.AuthAuditEventType;
|
||||
import com.project.auth.application.support.exception.AuthErrorCode;
|
||||
import com.project.auth.application.support.logging.LogSanitizer;
|
||||
import com.project.auth.config.auth.security.SecurityAuditTrailWriter;
|
||||
import com.project.auth.presentation.support.exception.ApiErrorHttpStatusMapper;
|
||||
import com.project.auth.presentation.support.response.ApiResult;
|
||||
import com.project.auth.presentation.support.response.ApiResultFactory;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Spring Security 예외를 처리하는 bootstrap 계층 advice.
|
||||
*
|
||||
* presentation 모듈은 ArchUnit 규칙에 의해 org.springframework.security.core / context에
|
||||
* 직접 의존할 수 없으므로(LayerDependencyArchitectureTest 참고), 이 advice는 bootstrap에 둔다.
|
||||
*
|
||||
* AccessDeniedException은 현재 인증 상태에 따라 401/403으로 분기한다. 익명 principal이
|
||||
* 보호된 리소스에 접근한 경우는 인증이 필요하다는 의미로 401을 반환하고, 인증된 principal이
|
||||
* 권한이 부족한 경우에만 403을 반환한다. Spring Security의 ExceptionTranslationFilter가
|
||||
* 필터 단계에서 던지는 예외에 대해 적용하는 분기 로직을 컨트롤러 단(@PreAuthorize 등 메서드
|
||||
* 보안)에서 던져진 동일 예외에도 일관되게 적용한 것이다.
|
||||
*
|
||||
* Audit 정책: 이 advice의 모든 분기(인증 필요, 익명 401, 인증된 사용자 403)는
|
||||
* SecurityAuditTrailWriter로 audit를 기록한다. 필터 단의 SecurityExceptionHandler가
|
||||
* 기록하는 audit 채널과 동일 채널을 사용하므로, "필터 단/컨트롤러 단" 어느 경로로 들어와도
|
||||
* 보안 이벤트가 누락 없이 동일 형태로 적재된다.
|
||||
*
|
||||
* 스레드/익명 판정: SecurityContextHolder의 ThreadLocal 컨텍스트를 우선 보지만,
|
||||
* 비동기 컨트롤러로 컨텍스트가 워커 스레드로 전파되지 않은 케이스를 대비해
|
||||
* HttpServletRequest.getUserPrincipal()을 보조 신호로 사용한다. principal이 명시적으로
|
||||
* 존재하면 익명으로 분류하지 않는다 — 컨텍스트가 비어 있더라도 인증된 사용자가 잘못
|
||||
* 401로 분류되지 않도록 보호하는 방어적 폴백이다.
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 5)
|
||||
public class SecurityResponseExceptionHandler {
|
||||
|
||||
<span class="fc" id="L53"> private static final Logger log = LoggerFactory.getLogger(SecurityResponseExceptionHandler.class);</span>
|
||||
|
||||
private final ApiResultFactory apiResultFactory;
|
||||
private final SecurityAuditTrailWriter securityAuditTrailWriter;
|
||||
|
||||
public SecurityResponseExceptionHandler(
|
||||
ApiResultFactory apiResultFactory,
|
||||
SecurityAuditTrailWriter securityAuditTrailWriter
|
||||
<span class="fc" id="L61"> ) {</span>
|
||||
<span class="fc" id="L62"> this.apiResultFactory = Objects.requireNonNull(apiResultFactory, "apiResultFactory must not be null");</span>
|
||||
<span class="fc" id="L63"> this.securityAuditTrailWriter = Objects.requireNonNull(</span>
|
||||
securityAuditTrailWriter, "securityAuditTrailWriter must not be null");
|
||||
<span class="fc" id="L65"> }</span>
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public ResponseEntity<ApiResult<Void>> handleAuthenticationException(
|
||||
AuthenticationException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
<span class="fc" id="L72"> log.warn("Authentication required. exceptionType={} method={} requestPath={} errorCode={}",</span>
|
||||
<span class="fc" id="L73"> exception.getClass().getSimpleName(),</span>
|
||||
<span class="fc" id="L74"> request.getMethod(),</span>
|
||||
<span class="fc" id="L75"> LogSanitizer.requestPath(request.getRequestURI()),</span>
|
||||
<span class="fc" id="L76"> AuthErrorCode.AUTHENTICATION_REQUIRED.code());</span>
|
||||
<span class="fc" id="L77"> recordAudit(request, AuthAuditEventType.AUTHENTICATION_REQUIRED, "Authentication required.");</span>
|
||||
|
||||
<span class="fc" id="L79"> return ResponseEntity.status(ApiErrorHttpStatusMapper.map(AuthErrorCode.AUTHENTICATION_REQUIRED))</span>
|
||||
<span class="fc" id="L80"> .body(apiResultFactory.failure(</span>
|
||||
<span class="fc" id="L81"> AuthErrorCode.AUTHENTICATION_REQUIRED.code(),</span>
|
||||
<span class="fc" id="L82"> AuthErrorCode.AUTHENTICATION_REQUIRED.message()</span>
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<ApiResult<Void>> handleAccessDeniedException(
|
||||
AccessDeniedException exception,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
<span class="pc bpc" id="L91" title="1 of 2 branches missed."> if (isAnonymous(SecurityContextHolder.getContext().getAuthentication(), request)) {</span>
|
||||
<span class="nc" id="L92"> log.warn("AccessDenied for anonymous principal -> 401. exceptionType={} method={} requestPath={} errorCode={}",</span>
|
||||
<span class="nc" id="L93"> exception.getClass().getSimpleName(),</span>
|
||||
<span class="nc" id="L94"> request.getMethod(),</span>
|
||||
<span class="nc" id="L95"> LogSanitizer.requestPath(request.getRequestURI()),</span>
|
||||
<span class="nc" id="L96"> AuthErrorCode.AUTHENTICATION_REQUIRED.code());</span>
|
||||
<span class="nc" id="L97"> recordAudit(request, AuthAuditEventType.AUTHENTICATION_REQUIRED,</span>
|
||||
"Authentication required (AccessDenied for anonymous).");
|
||||
|
||||
<span class="nc" id="L100"> return ResponseEntity.status(ApiErrorHttpStatusMapper.map(AuthErrorCode.AUTHENTICATION_REQUIRED))</span>
|
||||
<span class="nc" id="L101"> .body(apiResultFactory.failure(</span>
|
||||
<span class="nc" id="L102"> AuthErrorCode.AUTHENTICATION_REQUIRED.code(),</span>
|
||||
<span class="nc" id="L103"> AuthErrorCode.AUTHENTICATION_REQUIRED.message()</span>
|
||||
));
|
||||
}
|
||||
|
||||
<span class="fc" id="L107"> log.warn("Access denied for authenticated principal. exceptionType={} method={} requestPath={} errorCode={}",</span>
|
||||
<span class="fc" id="L108"> exception.getClass().getSimpleName(),</span>
|
||||
<span class="fc" id="L109"> request.getMethod(),</span>
|
||||
<span class="fc" id="L110"> LogSanitizer.requestPath(request.getRequestURI()),</span>
|
||||
<span class="fc" id="L111"> AuthErrorCode.ACCESS_DENIED.code());</span>
|
||||
<span class="fc" id="L112"> recordAudit(request, AuthAuditEventType.ACCESS_DENIED, "Access denied.");</span>
|
||||
|
||||
<span class="fc" id="L114"> return ResponseEntity.status(ApiErrorHttpStatusMapper.map(AuthErrorCode.ACCESS_DENIED))</span>
|
||||
<span class="fc" id="L115"> .body(apiResultFactory.failure(</span>
|
||||
<span class="fc" id="L116"> AuthErrorCode.ACCESS_DENIED.code(),</span>
|
||||
<span class="fc" id="L117"> AuthErrorCode.ACCESS_DENIED.message()</span>
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* SecurityContext가 비어 있어도 서블릿 principal이 명시적으로 존재하면 익명이 아니다.
|
||||
* ThreadLocal 컨텍스트가 비동기 워커 스레드로 전파되지 않은 케이스에서 인증된 사용자가
|
||||
* 잘못 401로 분류되는 사고를 막기 위한 방어적 폴백.
|
||||
*/
|
||||
private static boolean isAnonymous(Authentication authentication, HttpServletRequest request) {
|
||||
<span class="pc bpc" id="L127" title="1 of 2 branches missed."> if (request.getUserPrincipal() != null) {</span>
|
||||
<span class="fc" id="L128"> return false;</span>
|
||||
}
|
||||
<span class="nc bnc" id="L130" title="All 2 branches missed."> return authentication == null</span>
|
||||
<span class="nc bnc" id="L131" title="All 4 branches missed."> || !authentication.isAuthenticated()</span>
|
||||
|| authentication instanceof AnonymousAuthenticationToken;
|
||||
}
|
||||
|
||||
private void recordAudit(HttpServletRequest request, AuthAuditEventType type, String description) {
|
||||
try {
|
||||
<span class="fc" id="L137"> securityAuditTrailWriter.record(request, type, description);</span>
|
||||
<span class="nc" id="L138"> } catch (RuntimeException auditFailure) {</span>
|
||||
<span class="nc" id="L139"> log.warn("Security audit write failed; continuing with response rendering. type={}",</span>
|
||||
type, auditFailure);
|
||||
<span class="fc" id="L141"> }</span>
|
||||
<span class="fc" id="L142"> }</span>
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>TraceIdFilter</title><script type="text/javascript" src="../jacoco-resources/sort.js"></script></head><body onload="initialSort(['breadcrumb'])"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.html" class="el_package">com.project.auth.config.web</a> > <span class="el_class">TraceIdFilter</span></div><h1>TraceIdFilter</h1><table class="coverage" cellspacing="0" id="coveragetable"><thead><tr><td class="sortable" id="a" onclick="toggleSort(this)">Element</td><td class="down sortable bar" id="b" onclick="toggleSort(this)">Missed Instructions</td><td class="sortable ctr2" id="c" onclick="toggleSort(this)">Cov.</td><td class="sortable bar" id="d" onclick="toggleSort(this)">Missed Branches</td><td class="sortable ctr2" id="e" onclick="toggleSort(this)">Cov.</td><td class="sortable ctr1" id="f" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="g" onclick="toggleSort(this)">Cxty</td><td class="sortable ctr1" id="h" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="i" onclick="toggleSort(this)">Lines</td><td class="sortable ctr1" id="j" onclick="toggleSort(this)">Missed</td><td class="sortable ctr2" id="k" onclick="toggleSort(this)">Methods</td></tr></thead><tfoot><tr><td>Total</td><td class="bar">4 of 61</td><td class="ctr2">93%</td><td class="bar">3 of 4</td><td class="ctr2">25%</td><td class="ctr1">2</td><td class="ctr2">6</td><td class="ctr1">0</td><td class="ctr2">15</td><td class="ctr1">0</td><td class="ctr2">4</td></tr></tfoot><tbody><tr><td id="a1"><a href="TraceIdFilter.java.html#L47" class="el_method">nextTraceId()</a></td><td class="bar" id="b0"><img src="../jacoco-resources/redbar.gif" width="14" height="10" title="4" alt="4"/><img src="../jacoco-resources/greenbar.gif" width="65" height="10" title="18" alt="18"/></td><td class="ctr2" id="c3">81%</td><td class="bar" id="d0"><img src="../jacoco-resources/redbar.gif" width="90" height="10" title="3" alt="3"/><img src="../jacoco-resources/greenbar.gif" width="30" height="10" title="1" alt="1"/></td><td class="ctr2" id="e0">25%</td><td class="ctr1" id="f0">2</td><td class="ctr2" id="g0">3</td><td class="ctr1" id="h0">0</td><td class="ctr2" id="i1">4</td><td class="ctr1" id="j0">0</td><td class="ctr2" id="k0">1</td></tr><tr><td id="a0"><a href="TraceIdFilter.java.html#L30" class="el_method">doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)</a></td><td class="bar" id="b1"><img src="../jacoco-resources/greenbar.gif" width="120" height="10" title="33" alt="33"/></td><td class="ctr2" id="c0">100%</td><td class="bar" id="d1"/><td class="ctr2" id="e1">n/a</td><td class="ctr1" id="f1">0</td><td class="ctr2" id="g1">1</td><td class="ctr1" id="h1">0</td><td class="ctr2" id="i0">9</td><td class="ctr1" id="j1">0</td><td class="ctr2" id="k1">1</td></tr><tr><td id="a3"><a href="TraceIdFilter.java.html#L16" class="el_method">TraceIdFilter()</a></td><td class="bar" id="b2"><img src="../jacoco-resources/greenbar.gif" width="10" height="10" title="3" alt="3"/></td><td class="ctr2" id="c1">100%</td><td class="bar" id="d2"/><td class="ctr2" id="e2">n/a</td><td class="ctr1" id="f2">0</td><td class="ctr2" id="g2">1</td><td class="ctr1" id="h2">0</td><td class="ctr2" id="i2">1</td><td class="ctr1" id="j2">0</td><td class="ctr2" id="k2">1</td></tr><tr><td id="a2"><a href="TraceIdFilter.java.html#L23" class="el_method">static {...}</a></td><td class="bar" id="b3"><img src="../jacoco-resources/greenbar.gif" width="10" height="10" title="3" alt="3"/></td><td class="ctr2" id="c2">100%</td><td class="bar" id="d3"/><td class="ctr2" id="e3">n/a</td><td class="ctr1" id="f3">0</td><td class="ctr2" id="g3">1</td><td class="ctr1" id="h3">0</td><td class="ctr2" id="i3">1</td><td class="ctr1" id="j3">0</td><td class="ctr2" id="k3">1</td></tr></tbody></table><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>TraceIdFilter.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">bootstrap</a> > <a href="index.source.html" class="el_package">com.project.auth.config.web</a> > <span class="el_source">TraceIdFilter.java</span></div><h1>TraceIdFilter.java</h1><pre class="source lang-java linenums">package com.project.auth.config.web;
|
||||
|
||||
import com.project.auth.application.support.logging.LogSanitizer;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
import org.slf4j.MDC.MDCCloseable;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
<span class="fc" id="L16">public class TraceIdFilter extends OncePerRequestFilter {</span>
|
||||
|
||||
private static final String TRACE_ID_KEY = "traceId";
|
||||
private static final String CLIENT_IP_KEY = "clientIp";
|
||||
private static final String USER_AGENT_KEY = "userAgent";
|
||||
private static final String TRACE_ID_HEADER = "X-Trace-Id";
|
||||
private static final String USER_AGENT_HEADER = "User-Agent";
|
||||
<span class="fc" id="L23"> private static final HexFormat HEX_FORMAT = HexFormat.of();</span>
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
<span class="fc" id="L30"> String traceId = nextTraceId();</span>
|
||||
<span class="fc" id="L31"> String clientIp = LogSanitizer.clientIp(request.getRemoteAddr());</span>
|
||||
<span class="fc" id="L32"> String userAgent = LogSanitizer.userAgent(request.getHeader(USER_AGENT_HEADER));</span>
|
||||
<span class="fc" id="L33"> response.setHeader(TRACE_ID_HEADER, traceId);</span>
|
||||
|
||||
try (
|
||||
<span class="fc" id="L36"> MDCCloseable ignoredTraceId = MDC.putCloseable(TRACE_ID_KEY, traceId);</span>
|
||||
<span class="fc" id="L37"> MDCCloseable ignoredClientIp = MDC.putCloseable(CLIENT_IP_KEY, clientIp);</span>
|
||||
<span class="fc" id="L38"> MDCCloseable ignoredUserAgent = MDC.putCloseable(USER_AGENT_KEY, userAgent)) {</span>
|
||||
<span class="fc" id="L39"> filterChain.doFilter(request, response);</span>
|
||||
}
|
||||
<span class="fc" id="L41"> }</span>
|
||||
|
||||
private String nextTraceId() {
|
||||
long highBits;
|
||||
long lowBits;
|
||||
do {
|
||||
<span class="fc" id="L47"> highBits = ThreadLocalRandom.current().nextLong();</span>
|
||||
<span class="fc" id="L48"> lowBits = ThreadLocalRandom.current().nextLong();</span>
|
||||
<span class="pc bpc" id="L49" title="3 of 4 branches missed."> } while (highBits == 0L && lowBits == 0L);</span>
|
||||
|
||||
<span class="fc" id="L51"> return HEX_FORMAT.toHexDigits(highBits) + HEX_FORMAT.toHexDigits(lowBits);</span>
|
||||
}
|
||||
|
||||
}
|
||||
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.13.202504020838</span></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user