Статьи

Контролируйте свои Java-приложения с помощью Spring Boot Actuator

Друзья не позволяют друзьям писать аутентификацию пользователя. Надоело управлять своими пользователями? Попробуйте API Okta и Java SDK сегодня. Аутентификация, управление и защита пользователей в любом приложении в течение нескольких минут.

Вы уже работали с Spring Boot Actuator? Это очень полезная библиотека, которая помогает вам следить за состоянием приложения и взаимодействием с приложением — идеально подходит для запуска в производство! Spring Boot Actuator включает в себя встроенную конечную точку для отслеживания HTTP-вызовов к вашему приложению — очень полезно для мониторинга запросов OpenID Connect (OIDC) — но, к сожалению, реализация по умолчанию не отслеживает содержимое тела. В этой статье я покажу, как расширить конечную точку httptrace для захвата содержимого и отслеживания потока OIDC.

Давайте начнем!

Создайте приложение OpenID Connect с Spring Initializr и Okta

Вы можете использовать отличный веб-сайт Spring API или API для создания примера приложения OIDC с интеграцией Okta:

curl https://start.spring.io/starter.zip \
  dependencies==web,okta \
  packageName==com.okta.developer.demo -d

Однако перед запуском приложения OIDC вам потребуется учетная запись Okta. Okta — это сервис для разработчиков, который занимается хранением учетных записей пользователей и реализацией управления пользователями (включая OIDC). Чтобы продолжить, зарегистрируйтесь на бесплатной учетной записи разработчика .

После входа в свою учетную запись Okta перейдите на панель инструментов, а затем в раздел « Приложения ». Добавьте новое веб-приложение, а затем в разделе Общие получите учетные данные клиента: Client ID и Client Secret .

Вам также понадобится Эмитент, который также является URL-адресом организации, который вы можете найти в правом верхнем углу главной панели. Примечание . По умолчанию этому приложению назначена встроенная группа Everyone Okta, поэтому все пользователи вашей организации Okta смогут проходить проверку подлинности.

С вашим идентификатором клиента, секрет клиента. и эмитент на месте, запустите ваше приложение, передав учетные данные через командную строку:

OKTA_OAUTH2_REDIRECTURI=/authorization-code/callback \
OKTA_OAUTH2_ISSUER=<issuer>/oauth2 \
OKTA_OAUTH2_CLIENT_ID=<client id> \
OKTA_OAUTH2_CLIENT_SECRET=<client secret> \
./mvnw spring-boot:run

Добавить тестовый контроллер в приложение Spring Boot

Хорошей практикой является добавление простого контроллера для проверки потока аутентификации. По умолчанию доступ будет разрешен только аутентифицированным пользователям.

@Controller
@RequestMapping(value = "/hello")
public class HelloController {
 
    @GetMapping(value = "/greeting")
    @ResponseBody
    public String getGreeting(Principal user) {
        return "Good morning " + user.getName();
    }
}

Вы можете проверить это, перезапустив приложение и перейдя в / привет / приветствие .

Добавить зависимость привода загрузочной пружины

Включите Spring Boot Actuator, добавив pom.xml file Maven-зависимость в pom.xml file :

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Чтобы включить конечную точку httptrace, отредактируйте файл src/main/resources/application.properties и добавьте следующую строку:

management.endpoints.web.exposure.include=info,health,httptrace

Вы можете протестировать готовые функции привода, запустив приложение, перейдя в / hello / приветствие и войдя в систему.

При автоматической настройке фильтры Spring Security имеют более высокий приоритет, чем фильтры, добавленные приводом httptrace.

Это означает, что только аутентифицированные вызовы отслеживаются по умолчанию. Мы собираемся изменить это здесь в ближайшее время, но сейчас вы можете увидеть, что отслеживается в / actator / httptrace . Ответ должен выглядеть следующим образом:

{
   "traces":[
      {
         "timestamp":"2019-05-19T05:38:42.726Z",
         "principal":{
            "name":"***"
         },
         "session":{
            "id":"***"
         },
         "request":{
            "method":"GET",
            "uri":"http://localhost:8080/",
            "headers":{},
            "remoteAddress":"0:0:0:0:0:0:0:1"
         },
         "response":{
            "status":200,
            "headers":{}
         },
         "timeTaken":145
      }
   ]
}

Добавьте пользовательскую трассировку HTTP в приложение Spring Boot

Трассировка HTTP не очень гибкая. Энди Уилкинсон, автор привода httptrace, предлагает реализовать собственную конечную точку, если требуется отслеживание тела.

В качестве альтернативы, с помощью некоторых пользовательских фильтров мы можем улучшить базовую реализацию без особых усилий. В следующих разделах я покажу вам, как:

  • Создать фильтр для захвата запроса и тела ответа
  • Настройте приоритет фильтров для отслеживания вызовов OIDC
  • Создайте расширение конечной точки httptrace с настраиваемым хранилищем трассировки для хранения дополнительных данных.

Использование Spring Boot Actuator для захвата содержимого тела запроса и ответа

Затем создайте фильтр для отслеживания содержимого тела запроса и ответа. Этот фильтр будет иметь приоритет над фильтром httptrace, поэтому содержимое кэшированного тела будет доступно, когда привод сохранит трассу.

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class ContentTraceFilter extends OncePerRequestFilter {
 
    private ContentTraceManager traceManager;
 
    @Value("${management.trace.http.tracebody:false}")
    private boolean traceBody;
 
   public ContentTraceFilter(ContentTraceManager traceManager) {
        super();
        this.traceManager = traceManager;
    }
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
 
        if (!isRequestValid(request) || !traceBody) {
            filterChain.doFilter(request, response);
            return;
        }
 
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(
                request, 1000);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(
                response);
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
            traceManager.updateBody(wrappedRequest, wrappedResponse);
        } finally {
            wrappedResponse.copyBodyToResponse();
        }
    }
 
    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }
 
}

Обратите внимание на вызов @RequestScope простого бина @RequestScope который будет хранить дополнительные данные:

@Component
@RequestScope
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class ContentTraceManager {
 
    private ContentTrace trace;
 
    public ContentTraceManager(ContentTrace trace) {
        this.trace=trace;
    }
 
    protected static Logger logger = LoggerFactory
            .getLogger(ContentTraceManager.class);
 
    public void updateBody(ContentCachingRequestWrapper wrappedRequest,
            ContentCachingResponseWrapper wrappedResponse) {
 
        String requestBody = getRequestBody(wrappedRequest);
        getTrace().setRequestBody(requestBody);
 
        String responseBody = getResponseBody(wrappedResponse);
        getTrace().setResponseBody(responseBody);
    }
 
    protected String getRequestBody(
            ContentCachingRequestWrapper wrappedRequest) {
        try {
            if (wrappedRequest.getContentLength() <= 0) {
                return null;
            }
            return new String(wrappedRequest.getContentAsByteArray(), 0,
                    wrappedRequest.getContentLength(),
                    wrappedRequest.getCharacterEncoding());
        } catch (UnsupportedEncodingException e) {
            logger.error(
                    "Could not read cached request body: " + e.getMessage());
            return null;
        }
 
    }
 
    protected String getResponseBody(
            ContentCachingResponseWrapper wrappedResponse) {
 
        try {
            if (wrappedResponse.getContentSize() <= 0) {
                return null;
            }
            return new String(wrappedResponse.getContentAsByteArray(), 0,
                    wrappedResponse.getContentSize(),
                    wrappedResponse.getCharacterEncoding());
        } catch (UnsupportedEncodingException e) {
            logger.error(
                    "Could not read cached response body: " + e.getMessage());
            return null;
        }
 
    }
 
    public ContentTrace getTrace() {
        if (trace == null) {
            trace = new ContentTrace();
        }
        return trace;
    }
}

Для моделирования трассировки с дополнительными данными HttpTrace пользовательский класс HttpTrace со встроенной информацией HttpTrace , добавив свойства для хранения содержимого тела.

public class ContentTrace {
 
    protected HttpTrace httpTrace;
 
    protected String requestBody;
 
    protected String responseBody;
 
    protected Authentication principal;
 
    public ContentTrace() {
    }
 
    public void setHttpTrace(HttpTrace httpTrace) {
        this.httpTrace = httpTrace;
    }
}

Добавьте сеттеры и геттеры для httpTrace , requestBody , requestBody и responseBody .

Настроить приоритет фильтра

Для сбора запросов к конечным точкам OIDC в ​​вашем приложении фильтры трассировки должны находиться перед фильтрами Spring Security. Пока HttpTraceFilter имеет приоритет над HttpTraceFilter , оба могут быть размещены до или после SecurityContextPersistenceFilter , первого в цепочке фильтров Spring Security.

@Configuration
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private HttpTraceFilter httpTraceFilter;
    private ContentTraceFilter contentTraceFilter;
 
    public WebSecurityConfig(
        HttpTraceFilter httpTraceFilter, ContentTraceFilter contentTraceFilter
    ) {
        this.httpTraceFilter = httpTraceFilter;
        this.contentTraceFilter = contentTraceFilter;
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(contentTraceFilter,
                SecurityContextPersistenceFilter.class)
                .addFilterAfter(httpTraceFilter,
                        SecurityContextPersistenceFilter.class)
                .authorizeRequests().anyRequest().authenticated()
                .and().oauth2Client()
                .and().oauth2Login();
    }
}

Отслеживание аутентифицированного пользователя

Мы устанавливаем фильтры трассировки перед цепочкой фильтров Spring Security. Это означает, что Принципал больше не доступен, когда HttpTraceFilter сохраняет трассировку. Мы можем восстановить эти данные трассировки с помощью нового фильтра и ContentTraceManager.

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class PrincipalTraceFilter extends OncePerRequestFilter {
 
    private ContentTraceManager traceManager;
    private HttpTraceProperties traceProperties;
 
    public PrincipalTraceFilter(
        ContentTraceManager traceManager,
        HttpTraceProperties traceProperties
    ) {
        super();
        this.traceManager = traceManager;
        this.traceProperties = traceProperties;
    }
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {
 
        if (!isRequestValid(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            filterChain.doFilter(request, response);
 
        } finally {
            if (traceProperties.getInclude().contains(Include.PRINCIPAL)) {
                traceManager.updatePrincipal();
            }
        }
 
    }
 
    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }
 
}

Добавьте отсутствующий класс ContentTraceManager для обновления принципала:

public class ContentTraceManager {
 
    public void updatePrincipal() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            getTrace().setPrincipal(authentication);
        }
  }
}

PrincipalTraceFilter должен иметь более низкий приоритет, чем цепочка фильтров Spring Security, поэтому аутентифицированный принципал доступен по запросу из контекста безопасности. Измените WebSecurityConfig чтобы вставить фильтр после FilterSecurityInterceptor , последнего фильтра в цепочке безопасности.

@Configuration
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private HttpTraceFilter httpTraceFilter;
    private ContentTraceFilter contentTraceFilter;
    private PrincipalTraceFilter principalTraceFilter;
 
    public WebSecurityConfig(
        HttpTraceFilter httpTraceFilter,
        ContentTraceFilter contentTraceFilter,
        PrincipalTraceFilter principalTraceFilter
    ) {
        super();
        this.httpTraceFilter = httpTraceFilter;
        this.contentTraceFilter = contentTraceFilter;
        this.principalTraceFilter = principalTraceFilter;
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(contentTraceFilter,
                SecurityContextPersistenceFilter.class)
                .addFilterAfter(httpTraceFilter,
                        SecurityContextPersistenceFilter.class)
                .addFilterAfter(principalTraceFilter,
                        FilterSecurityInterceptor.class)
                .authorizeRequests().anyRequest().authenticated()
                .and().oauth2Client()
                .and().oauth2Login();
    }
}

Расширение конечной точки HTTPTrace

Наконец, определите расширение конечной точки, используя аннотацию @EndpointWebExtension . CustomHttpTraceRepository для хранения и извлечения ContentTrace с дополнительными данными.

@Component
@EndpointWebExtension(endpoint = HttpTraceEndpoint.class)
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class HttpTraceEndpointExtension {
 
    private CustomHttpTraceRepository repository;
 
    public HttpTraceEndpointExtension(CustomHttpTraceRepository repository) {
        super();
        this.repository = repository;
    }
 
    @ReadOperation
    public ContentTraceDescriptor contents() {
        List<ContentTrace> traces = repository.findAllWithContent();
        return new ContentTraceDescriptor(traces);
    }
}

Переопределите дескриптор для типа возврата конечной точки:

public class ContentTraceDescriptor {
 
    protected List<ContentTrace> traces;
 
    public ContentTraceDescriptor(List<ContentTrace> traces) {
        super();
        this.traces = traces;
    }
 
    public List<ContentTrace> getTraces() {
        return traces;
    }
 
    public void setTraces(List<ContentTrace> traces) {
        this.traces = traces;
    }
 
}

Создайте CustomHttpTraceRepository реализующий интерфейс HttpTraceRepository :

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class CustomHttpTraceRepository implements HttpTraceRepository {
 
    private final List<ContentTrace> contents = new LinkedList<>();
 
    private ContentTraceManager traceManager;
 
    public CustomHttpTraceRepository(ContentTraceManager traceManager) {
        super();
        this.traceManager = traceManager;
    }
 
    @Override
    public void add(HttpTrace trace) {
        synchronized (this.contents) {
            ContentTrace contentTrace = traceManager.getTrace();
            contentTrace.setHttpTrace(trace);
            this.contents.add(0, contentTrace);
        }
    }
 
    @Override
    public List<HttpTrace> findAll() {
        synchronized (this.contents) {
            return contents.stream().map(ContentTrace::getHttpTrace)
                    .collect(Collectors.toList());
        }
    }
 
    public List<ContentTrace> findAllWithContent() {
        synchronized (this.contents) {
            return Collections.unmodifiableList(new ArrayList<>(this.contents));
        }
    }
 
}

Проверка трассировки OpenID Connect HTTP

Измените файл application.properties для отслеживания всех доступных данных, добавив следующую строку:

management.trace.http.include=request-headers,response-headers,cookie-headers,principal,time-taken,authorization-header,remote-address,session-id

Запустите приложение еще раз и вызовите защищенный контроллер / привет / приветствие . Выполните аутентификацию в Okta, а затем просмотрите следы в / actator / httptrace .

Теперь вы должны увидеть вызовы OIDC в ​​трассировке, а также содержимое запросов и ответов. Например, в приведенной ниже трассировке запрос к конечной точке авторизации приложения перенаправляет на сервер авторизации Okta, инициируя поток кода авторизации OIDC.

{
    "httpTrace": {
        "timestamp": "2019-05-22T00:52:22.383Z",
        "principal": null,
        "session": {
            "id": "C2174F5E5F85B313B2284639EE4016E7"
        },
        "request": {
            "method": "GET",
            "uri": "http://localhost:8080/oauth2/authorization/okta",
            "headers": {
                "cookie": [
                    "JSESSIONID=C2174F5E5F85B313B2284639EE4016E7"
                ],
                "accept-language": [
                    "en-US,en;q=0.9"
                ],
                "upgrade-insecure-requests": [
                    "1"
                ],
                "host": [
                    "localhost:8080"
                ],
                "connection": [
                    "keep-alive"
                ],
                "accept-encoding": [
                    "gzip, deflate, br"
                ],
                "accept": [
                    "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
                ],
                "user-agent": [
                    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
                ]
            },
            "remoteAddress": "0:0:0:0:0:0:0:1"
        },
        "response": {
            "status": 302,
            "headers": {
                "X-Frame-Options": [
                    "DENY"
                ],
                "Cache-Control": [
                    "no-cache, no-store, max-age=0, must-revalidate"
                ],
                "X-Content-Type-Options": [
                    "nosniff"
                ],
                "Expires": [
                    "0"
                ],
                "Pragma": [
                    "no-cache"
                ],
                "X-XSS-Protection": [
                    "1; mode=block"
                ],
                "Location": [
                ]
            }
        },
        "timeTaken": 9
    },
    "requestBody": null,
    "responseBody": null
}

Весь код в этом посте можно найти на GitHub в репозитории okta-spring-boot-custom-actator-example .

Учить больше

Это все, что нужно сделать! Вы только что узнали, как настроить и расширить httptrace точку привода httptrace для мониторинга вашего приложения httptrace . Для получения дополнительной информации о Spring Boot Actuator, Spring Boot в целом или аутентификации пользователей, перейдите по ссылкам ниже:

Как всегда, если у вас есть какие-либо комментарии или вопросы по поводу этого поста, не стесняйтесь комментировать ниже. Не пропустите ни одного из наших интересных материалов в будущем, подписавшись на нас в Twitter и YouTube .

«Мониторинг Java-приложений с помощью Spring Boot Actuator» был впервые опубликован в блоге Okta Developer 17 июля 2019 года.

Друзья не позволяют друзьям писать аутентификацию пользователя. Надоело управлять своими пользователями? Попробуйте API Okta и Java SDK сегодня. Аутентификация, управление и защита пользователей в любом приложении в течение нескольких минут.