Trace OAuth Requests
Failed to find access token
OAuth API ์์ฒญ ์ JdbcTokenStore์์ ์ก์ธ์ค ํ ํฐ์ ์ฐพ์ ์ ์์ ๋ ๊ธฐ๋ก๋๋ INFO ๋ ๋ฒจ์ ๋ก๊ทธ ์ ๋๋ค. ์ก์ธ์ค ํ ํฐ์ ์ฐพ์ ์ ์๋ค๋ ์ด์ผ๊ธฐ๋ ์ฌ๋ฐ๋ฅด์ง ์์ ์์ฒญ์ธ๋ฐ๋ ๋ถ๊ตฌํ๊ณ INFO ๋ ๋ฒจ๋ก ๋์ด์๋ ๋ถ๋ถ์ ๋ํด์๋ ์์ํ๊ธด ํฉ๋๋ค๋ง ์ ์ ๋ณด๋ง์ผ๋ก๋ ์ด๋ค ํ ํฐ์ ์ํด์ ์ด๋ ํ OAuth API์ ๋ํด ์์ฒญ๋์๋์ง๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
์ผ๋ฐ์ ์ผ๋ก ์ธํ๋ผ ๋ ๋ฒจ์์ ELB ๋๋ Nginx์ ๊ฐ์ ํ๋ก์ ๋จ๊ณ์์ ์ก์ธ์ค ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋๋ฐ ๋๋ถ๋ถ์ ์ก์ธ์ค ๋ก๊ทธ์์๋ ์์ฒญ ํค๋ ์ ๋ณด๋ฅผ ์์ธํ๊ฒ ๊ธฐ๋กํ์ง ์๊ณ ๊ฐ๊ฒฐํ๊ฒ ๋จ๊ธฐ๋๋ก ์ค์ ๋๋ฏ๋ก ํ ํฐ ์ ๋ณด๊ฐ ํฌํจ๋๋ Authorization ํค๋๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
Bearer Authentication
ํ์ฌ ์์คํ ์ Spring Security OAuth ๋ชจ๋์ ํตํด JDBC ๊ธฐ๋ฐ์ Opaque ํ ํฐ์ผ๋ก ๋์ด์๋ Bearer ์ธ์ฆ์ ์ง์ํ๋ OpenAPI๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ๊ทธ๋ฌ๋, ์ผ๋ถ ์ฌ์ฉ์๋ค์ด IoT ๋๋ฐ์ด์ค๋ฅผ ๊ตฌํ ์ OpenAPI๋ฅผ ์ฌ์ฉํ ๋ ์๋ชป๋ ํ ํฐ์ ์ฌ์ฉํ๋ ๋ฌธ์ ๊ฐ ์ฌ์ ํ์ผ๋ก๋ถํฐ ๋ฆฌํฌํธ ๋์๋๋ฐ ๋จ์ํ๊ฒ ์๋ชป๋ ์ก์ธ์ค ํ ํฐ์ด ์ ๋ฌ๋์๋ค๋ ๋ก๊ทธ์ ๋ํด์ ์์ธ ํ์ ์ ์๊ตฌํ๋๋ฐ ๋ถ๊ตฌํ๊ณ ํ์ ํ ์ ์๋ ์ ๋ณด๊ฐ ๋จ์์์ง ์์์ต๋๋ค.
์ฌ์ค OpenAPI์ ๊ฐ ํธ๋ค๋ฌ๋ก ์ ๋ฌ๋๋ ๋ถ๋ถ์ ๋ํด์๋ AOP๊ฐ ์ ์ฉ๋์ด ํธ๋ค๋ฌ์ ๋ํ ํ๋ผ๋ฏธํฐ ๋ค๊ณผ ํด๋ผ์ด์ธํธ ์์ด๋์ ํ ํฐ ์ ๋ณด๋ฅผ ์ด๋ฏธ ์๋ผ์คํฑ์์น์ API ๋ก๊ทธ๋ก ์ ์ฅํ๊ณ ์์์ต๋๋ค. ๋ค๋ง ๋ฌธ์ ๋ ์๋ชป๋ ํ ํฐ์ ๋ํ ์์ฒญ์ ์คํ๋ง ์ํ๋ฆฌํฐ ํํฐ ์ฒด์ธ์ ์ํด์ ํธ๋ค๋ฌ๊น์ง ์ง์ ํ๊ธฐ ์ ์ ์ค๋ฅ ์๋ต์ผ๋ก ์ฒ๋ฆฌ๋์๊ธฐ์ API ๋ก๊ทธ๊ฐ ๋จ์ง ์์๋ค๋ ๊ฒ์ด ํ์ ๋์์ต๋๋ค.
์ด๋ ํ ๋ฌธ์ ์ ๋ํด์ ํ์ ํ ์ ์๋ ์ ๋ณด๋ ๋จ๊ฒจ์ผํ๋ฏ๋ก ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ OpenAPI๋ฅผ ์ฌ์ฉํ ๋ก๊ทธ ์ธ์ OAuth์ ๋ํ ๋ชจ๋ ์์ฒญ์ ๋ํด์๋ ๋ณ๋๋ก ๋จ๊ธฐ๋๋ก ๊ฐ์ ํ๊ณ ์ ํ์์ต๋๋ค.
OAuth2AuthenticationProcessingFilter
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
Bearer ํ ํฐ์ ๋ํ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ OAuth2AuthenticationProcessingFilter์์ AuthenticationEventPublisher๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ ์ฑ๊ณต์ด๋ ์ค๋ฅ์ ๋ํ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๋ฏ๋ก DefaultAuthenticationEventPublisher๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๊ณ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๊ตฌํํ๋ฉด ์ด๋ค ํ ํฐ์ ์ฌ์ฉํ์ฌ ์์ฒญํ๋์ง๋ ๊ธฐ๋กํ ์ ์๋๋ฐ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ก ์ ๋ฌ๋๋ ์ด๋ฒคํธ ์ ๋ณด์๋ ์์ฒญ๊ณผ ์๋ต์ ๋ํ ์ ๋ณด๊ฐ ์กด์ฌํ์ง ์์ผ๋ฏ๋ก ์ํ๋ ๋งํผ์ ์ ๋ณด๋ฅผ ๋ก๊ทธ๋ก ๊ธฐ๋กํ ์ ์์ต๋๋ค.
์ํ๋ ๊ธฐ๋ฅ์ ๋๋ถ๋ถ ์ธํฐ๋ท์ ๊ฒ์ํ๋ฉด ๋์ค๊ธฐ์ ์ด๋ฆฌ์ ๋ฆฌ ์ฐพ์๋ณด์์ต๋๋ค.
AbstractRequestLoggingFilter
AbstractRequestLoggingFilter.getMessagePayload ํจ์๋ฅผ ๋ณด๋ฉด ๋ด๋ถ์ ์ผ๋ก ContentCachingRequestWrapper๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ํ์ธํ ์ ์๋๋ฐ ContentCachingResponseWrapper์ ๋ํด์ ์ดํด๋ณด๋ ์์ฒญ์ ๋ํ ์๋ต์ ๋ด๋ ค์ค ์ดํ์๋ ์๋ต ํ์ด๋ก๋๋ฅผ ์ฝ์ ์ ์๋๋ก ์ง์ํ๋ค๋ ๋ด์ฉ์ ํ์ธํ์ต๋๋ค.
์๋ชป๋ ํ ํฐ์ ๋ํ ์์ฒญ์ ์๋ต์ผ๋ก ์ก์ธ์ค ํ ํฐ ์ ๋ณด๋ฅผ ์ ๋ฌํ๋ฏ๋ก ์๋ต ํ์ด๋ก๋๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ค๋ฉด ์ฌ์ฉ๋ ํ ํฐ์ ๋ก๊ทธ๋ก์จ ํ์ธํ ์ ์๋ค๋ ์ด์ผ๊ธฐ์ ๋๋ค.
HttpTraceFilter
Spring Boot Actuator ๋ชจ๋์ ์ฌ์ฉํ์ฌ ์ด๋๋ฏผ ํ์ด์ง๋ฅผ ๊ตฌํํด๋์๊ธฐ์ HttpTraceEndpoint๋ฅผ ํตํด์ ์ผ๋ถ ์์ฒญ์ ๋ํ ํธ๋ ์ด์ค ์ ๋ณด๋ฅผ ํ์ธํ ์ ์๋ ์ ์ ๋ ์ฌ๋ ค HttpTraceFilter๋ฅผ ์ดํด๋ณด๋ TraceableHttpServletRequest์ TraceableHttpServletResponse๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ๊ณผ ์๋ต์ ๋ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(request);
HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
filterChain.doFilter(request, response);
status = response.getStatus();
}
finally {
TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
(status != response.getStatus()) ? new CustomStatusResponseWrapper(response, status) : response);
this.tracer.sendingResponse(trace, traceableResponse, request::getUserPrincipal,
() -> getSessionId(request));
this.repository.add(trace);
}
}
๋จ, TraceableHttpServletRequest์ TraceableHttpServletResponse๋ final ํค์๋๊ฐ ์ค์ ๋ ํด๋์ค์ด๋ฏ๋ก ๋ค๋ฅธ ํจํค์ง์์ ํ์ฉํ ์ ์์ต๋๋ค.
OAuthFilter
์์ ์ดํด๋ณธ ํด๋์ค๋ค์ ์ข ํฉํ์ฌ OAuth ์์ฒญ๊ณผ ์๋ต์ ๋ํ ์ ๋ณด๋ฅผ ์ด๋ฒคํธ๋ก ๋ฐ์์ํค๋ ํํฐ๋ฅผ ๊ตฌํํฉ๋๋ค.
@Slf4j
@Component
public class OAuthFilter extends OncePerRequestFilter implements ApplicationEventPublisherAware {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final HttpExchangeTracer tracer;
private final DefaultTokenServices defaultTokenServices;
private ApplicationEventPublisher applicationEventPublisher;
public OAuthFilter(HttpTraceProperties traceProperties, DefaultTokenServices defaultTokenServices) {
this.tracer = new HttpExchangeTracer(traceProperties.getInclude());
this.defaultTokenServices = defaultTokenServices;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
String method = request.getMethod();
boolean isApiV1 = pathMatcher.match("/oauth/v1/**", requestURI);
if (isApiV1) {
TraceableHttpServletRequest traceableRequest = new TraceableHttpServletRequest(request);
ContentCachingResponseWrapper cachingResponseWrapper = new ContentCachingResponseWrapper(response);
HttpTrace trace = this.tracer.receivedRequest(traceableRequest);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
filterChain.doFilter(request, cachingResponseWrapper);
status = response.getStatus();
} finally {
TraceableHttpServletResponse traceableResponse = new TraceableHttpServletResponse(
(status != response.getStatus()) ? new CustomStatusResponseWrapper(cachingResponseWrapper, status) : cachingResponseWrapper);
this.tracer.sendingResponse(trace, traceableResponse, request::getUserPrincipal,
() -> getSessionId(request));
String clientId = null;
if (applicationEventPublisher != null) {
try {
String tokenValue = (String) request.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE);
OAuth2Authentication authentication = defaultTokenServices.loadAuthentication(tokenValue);
clientId = authentication.getOAuth2Request().getClientId();
} catch (InvalidTokenException ignored) {}
applicationEventPublisher.publishEvent(new OAuthTraceEvent(trace, traceableResponse.getResponseBody(), clientId));
}
}
} else {
filterChain.doFilter(request, response);
}
}
private String getSessionId(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return (session != null) ? session.getId() : null;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
static final class TraceableHttpServletRequest implements TraceableRequest {
private final HttpServletRequest request;
TraceableHttpServletRequest(HttpServletRequest request) {
this.request = request;
}
@Override
public String getMethod() {
return this.request.getMethod();
}
@Override
public URI getUri() {
String queryString = this.request.getQueryString();
if (!StringUtils.hasText(queryString)) {
return URI.create(this.request.getRequestURL().toString());
}
try {
StringBuffer urlBuffer = appendQueryString(queryString);
return new URI(urlBuffer.toString());
} catch (URISyntaxException ex) {
String encoded = UriUtils.encodeQuery(queryString, StandardCharsets.UTF_8);
StringBuffer urlBuffer = appendQueryString(encoded);
return URI.create(urlBuffer.toString());
}
}
private StringBuffer appendQueryString(String queryString) {
return this.request.getRequestURL().append("?").append(queryString);
}
@Override
public Map<String, List<String>> getHeaders() {
return extractHeaders();
}
@Override
public String getRemoteAddress() {
return this.request.getRemoteAddr();
}
private Map<String, List<String>> extractHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
headers.put(name, Collections.list(this.request.getHeaders(name)));
}
return headers;
}
}
static final class TraceableHttpServletResponse implements TraceableResponse {
private final HttpServletResponse delegate;
TraceableHttpServletResponse(HttpServletResponse response) {
this.delegate = response;
}
@Override
public int getStatus() {
return this.delegate.getStatus();
}
@Override
public Map<String, List<String>> getHeaders() {
return extractHeaders();
}
private Map<String, List<String>> extractHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
for (String name : this.delegate.getHeaderNames()) {
headers.put(name, new ArrayList<>(this.delegate.getHeaders(name)));
}
return headers;
}
public String getResponseBody() throws IOException {
if (this.delegate instanceof ContentCachingResponseWrapper) {
String body = null;
ContentCachingResponseWrapper wrapper = (ContentCachingResponseWrapper) this.delegate;
int status = wrapper.getStatus();
byte[] buf = wrapper.getContentAsByteArray();
if (status != 200 && buf.length > 0) {
body = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
}
wrapper.copyBodyToResponse();
return body;
}
return null;
}
}
private static final class CustomStatusResponseWrapper extends HttpServletResponseWrapper {
private final int status;
private CustomStatusResponseWrapper(HttpServletResponse response, int status) {
super(response);
this.status = status;
}
@Override
public int getStatus() {
return this.status;
}
}
}
์๋ชป๋ ํ ํฐ์ ๋ํ ์๋ต ์ฒ๋ฆฌ๋ OAuth2AuthenticationProcessingFilter์์ OAuthException์ ๋์ง๊ฒ ๋๋ฉด์ ์ํํ๋ฏ๋ก ์ฐ๋ฆฌ๋ ๊ทธ์ ์ ์๋ต ํ์ด๋ก๋๋ฅผ ์บ์ํ์ฌ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ContentCachingResponseWrapper๋ฅผ ์ดํ ํํฐ๋ก ์ ๋ฌํ์ต๋๋ค.
OAuthTraceEvent
OAuthTraceEvent๋ OAuth API์ ๋ํ ์์ฒญ๊ณผ ์๋ต ์ ๋ณด์์ ์ฌ๋ฐ๋ฅธ ํ ํฐ์ผ๋ก ์์ฒญ๋ ๊ฒ์ ํด๋ผ์ด์ธํธ ์์ด๋๊ฐ ์กด์ฌํ๋ฏ๋ก ์์ฒญ ํค๋ ์ค Authorization์ ํฌํจ๋ ํ ํฐ๊น์ง ๋ก๊ทธ๋ก ์ ์ฅํ ํ์๋ ์์ต๋๋ค. ํ ํฐ ์์ฒด๋ฅผ ์ ์ธํ๊ธฐ ๋ณด๋ค๋ ํ ํฐ์ ์ผ๋ถ๋ฅผ ๋ง์คํน ์ฒ๋ฆฌํ๋ ๋ฐฉํฅ์ผ๋ก ๊ตฌํํ์์ต๋๋ค.
@Getter
public class OAuthTraceEvent extends ApplicationEvent {
private final HttpTrace.Request request;
private final HttpTrace.Response response;
private final HttpTrace.Principal principal;
private final HttpTrace.Session session;
private final String responseBody;
private final String clientId;
private final long timeTaken;
private final long traceTimestamp;
public OAuthTraceEvent(HttpTrace trace, String responseBody, @Nullable String clientId) {
super(trace);
this.clientId = clientId;
if (clientId != null && !"" .equals(clientId)) {
protectAuthorization(trace);
}
this.timeTaken = trace.getTimeTaken();
this.request = trace.getRequest();
this.response = trace.getResponse();
this.principal = trace.getPrincipal();
this.session = trace.getSession();
this.responseBody = responseBody;
this.traceTimestamp = trace.getTimestamp().toEpochMilli();
}
private void protectAuthorization(HttpTrace trace) {
if (clientId != null && !"" .equals(clientId)) {
Map<String, List<String>> headers = trace.getRequest().getHeaders();
List<String> authorization = headers.get("Authorization");
for (int i = 0; i < authorization.size(); i++) {
String s = authorization.get(i);
if (s.startsWith("Bearer") || s.startsWith("bearer")) {
s = s.replaceAll("(?<=.{19}).", "*");
authorization.set(i, s);
}
}
}
}
}
์ด์ ์ฐ๋ฆฌ๋ OAuthTraceEvent๋ฅผ ์ฒ๋ฆฌํ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋์์ ์ ํ๋ฆฌ์ผ์ด์ ๋ก๊ทธ ๋๋ ์๋ผ์คํฑ์์น์ ์ ์ฅํ๋๋ก ๊ตฌํํ๋ฉด ๋ฉ๋๋ค.