init: 클린 기반 auth 서버 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:30:18 +09:00
parent 471db0203d
commit 8a1ac1e769
3642 changed files with 275893 additions and 1 deletions
File diff suppressed because one or more lines are too long
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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가
* &quot;요청이 잘못됐다&quot;&quot;서버가 깨졌다&quot;를 구분할 수 있게 한다.
*
* 의존성 정책: 이 컨트롤러는 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, &quot;apiResultFactory must not be null&quot;);</span>
<span class="fc" id="L46"> }</span>
@RequestMapping(&quot;/error&quot;)
ResponseEntity&lt;ApiResult&lt;Void&gt;&gt; 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() &amp;&amp; error != null) {</span>
<span class="nc" id="L57"> log.error(&quot;Unhandled exception. method={} requestPath={} status={}&quot;,</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(&quot;Unhandled error response. method={} requestPath={} status={}&quot;,</span>
<span class="fc" id="L61"> method, requestPath, httpStatus.value());</span>
} else {
<span class="fc" id="L63"> log.warn(&quot;Unhandled error response. method={} requestPath={} status={}&quot;,</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 -&gt; PresentationErrorCode.RESOURCE_NOT_FOUND;</span>
<span class="fc" id="L79"> case METHOD_NOT_ALLOWED -&gt; PresentationErrorCode.METHOD_NOT_ALLOWED;</span>
<span class="nc" id="L80"> case UNSUPPORTED_MEDIA_TYPE -&gt; PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE;</span>
<span class="nc" id="L81"> case NOT_ACCEPTABLE -&gt; PresentationErrorCode.NOT_ACCEPTABLE;</span>
<span class="nc" id="L82"> case CONTENT_TOO_LARGE -&gt; PresentationErrorCode.PAYLOAD_TOO_LARGE;</span>
<span class="fc" id="L83"> default -&gt; 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 &amp;&amp; !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.14.202510111229</span></div></body></html>
@@ -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">project-auth-server</a> &gt; <a href="index.html" class="el_package">com.project.auth.config.web</a> &gt; <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.14.202510111229</span></div></body></html>
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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 -&gt; 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&lt;ApiResult&lt;Void&gt;&gt; handleInfrastructureException(
InfrastructureException exception,
HttpServletRequest request
) {
<span class="fc" id="L42"> log.error(</span>
&quot;Infrastructure failure. errorCode={} method={} requestPath={}&quot;,
<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&lt;ApiResult&lt;Void&gt;&gt; handleLeakedDomainException(
DomainException exception,
HttpServletRequest request
) {
<span class="fc" id="L62"> log.error(</span>
&quot;Leaked domain exception. method={} requestPath={}&quot;,
<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.14.202510111229</span></div></body></html>
File diff suppressed because one or more lines are too long
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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(&quot;http.access&quot;);</span>
private static final String ACCESS_EVENT_TYPE = &quot;HTTP_ACCESS&quot;;
private final List&lt;String&gt; 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 &lt; 400 ? &quot;success&quot; : &quot;failure&quot;;</span>
<span class="fc" id="L69"> log.atInfo()</span>
<span class="fc" id="L70"> .addKeyValue(&quot;eventType&quot;, ACCESS_EVENT_TYPE)</span>
<span class="fc" id="L71"> .addKeyValue(&quot;method&quot;, method)</span>
<span class="fc" id="L72"> .addKeyValue(&quot;requestPath&quot;, path)</span>
<span class="fc" id="L73"> .addKeyValue(&quot;status&quot;, status)</span>
<span class="fc" id="L74"> .addKeyValue(&quot;durationMs&quot;, durationMs)</span>
<span class="fc" id="L75"> .addKeyValue(&quot;remoteIp&quot;, remoteIp)</span>
<span class="fc" id="L76"> .addKeyValue(&quot;actorId&quot;, actorId)</span>
<span class="fc" id="L77"> .addKeyValue(&quot;result&quot;, result)</span>
<span class="fc" id="L78"> .log(&quot;ACCESS&quot;);</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 &amp;&amp; authentication.isAuthenticated()</span>
<span class="pc bpc" id="L84" title="1 of 2 branches missed."> &amp;&amp; !&quot;anonymousUser&quot;.equals(authentication.getPrincipal())) {</span>
<span class="fc" id="L85"> return LogSanitizer.actorId(authentication.getName());</span>
}
<span class="fc" id="L87"> return LogSanitizer.actorId(&quot;anonymous&quot;);</span>
}
}
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.14.202510111229</span></div></body></html>
File diff suppressed because one or more lines are too long
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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 = &quot;traceId&quot;;
/**
* MDC에 traceId가 없을 때 사용하는 sentinel. JSON {@code null}로 그대로 두면
* TraceIdFilter가 누락/오설정된 사실이 가려지므로, 명시적인 placeholder 문자열로
* 로그·대시보드에서 즉시 식별 가능하게 한다.
*/
static final String MISSING_TRACE_ID = &quot;-&quot;;
<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, &quot;clock must not be null&quot;);</span>
<span class="fc" id="L28"> }</span>
@Override
public &lt;T&gt; ApiResult&lt;T&gt; success(String code, String message, T data) {
<span class="fc" id="L32"> return result(true, code, message, data, null);</span>
}
@Override
public ApiResult&lt;Void&gt; success(String code, String message) {
<span class="nc" id="L37"> return result(true, code, message, null, null);</span>
}
@Override
public ApiResult&lt;Void&gt; failure(String code, String message) {
<span class="fc" id="L42"> return result(false, code, message, null, null);</span>
}
@Override
public ApiResult&lt;Void&gt; failure(String code, String message, Map&lt;String, List&lt;String&gt;&gt; errors) {
<span class="fc" id="L47"> return result(false, code, message, null, errors);</span>
}
private &lt;T&gt; ApiResult&lt;T&gt; result(
boolean success,
String code,
String message,
T data,
Map&lt;String, List&lt;String&gt;&gt; errors
) {
<span class="fc" id="L57"> String traceId = MDC.get(TRACE_ID_KEY);</span>
<span class="fc" id="L58"> return new ApiResult&lt;&gt;(</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.14.202510111229</span></div></body></html>
File diff suppressed because one or more lines are too long
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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 채널과 동일 채널을 사용하므로, &quot;필터 단/컨트롤러 단&quot; 어느 경로로 들어와도
* 보안 이벤트가 누락 없이 동일 형태로 적재된다.
*
* 스레드/익명 판정: 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, &quot;apiResultFactory must not be null&quot;);</span>
<span class="fc" id="L63"> this.securityAuditTrailWriter = Objects.requireNonNull(</span>
securityAuditTrailWriter, &quot;securityAuditTrailWriter must not be null&quot;);
<span class="fc" id="L65"> }</span>
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity&lt;ApiResult&lt;Void&gt;&gt; handleAuthenticationException(
AuthenticationException exception,
HttpServletRequest request
) {
<span class="fc" id="L72"> log.warn(&quot;Authentication required. exceptionType={} method={} requestPath={} errorCode={}&quot;,</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, &quot;Authentication required.&quot;);</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&lt;ApiResult&lt;Void&gt;&gt; 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(&quot;AccessDenied for anonymous principal -&gt; 401. exceptionType={} method={} requestPath={} errorCode={}&quot;,</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>
&quot;Authentication required (AccessDenied for anonymous).&quot;);
<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(&quot;Access denied for authenticated principal. exceptionType={} method={} requestPath={} errorCode={}&quot;,</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, &quot;Access denied.&quot;);</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(&quot;Security audit write failed; continuing with response rendering. type={}&quot;,</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.14.202510111229</span></div></body></html>
@@ -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">project-auth-server</a> &gt; <a href="index.html" class="el_package">com.project.auth.config.web</a> &gt; <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.14.202510111229</span></div></body></html>
@@ -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">project-auth-server</a> &gt; <a href="index.source.html" class="el_package">com.project.auth.config.web</a> &gt; <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 = &quot;traceId&quot;;
private static final String CLIENT_IP_KEY = &quot;clientIp&quot;;
private static final String USER_AGENT_KEY = &quot;userAgent&quot;;
private static final String TRACE_ID_HEADER = &quot;X-Trace-Id&quot;;
private static final String USER_AGENT_HEADER = &quot;User-Agent&quot;;
<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 &amp;&amp; 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.14.202510111229</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