Друзья не позволяют друзьям писать аутентификацию пользователя. Надоело управлять своими пользователями? Попробуйте 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" , "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" , "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 в целом или аутентификации пользователей, перейдите по ссылкам ниже:
- Микросервисы Java с Spring Boot и Spring Cloud
- Конечные точки привода пружинной загрузки
- Реализация пользовательских конечных точек
- Краткое руководство по аутентификации Okta Java Spring
Как всегда, если у вас есть какие-либо комментарии или вопросы по поводу этого поста, не стесняйтесь комментировать ниже. Не пропустите ни одного из наших интересных материалов в будущем, подписавшись на нас в Twitter и YouTube .
«Мониторинг Java-приложений с помощью Spring Boot Actuator» был впервые опубликован в блоге Okta Developer 17 июля 2019 года.
Друзья не позволяют друзьям писать аутентификацию пользователя. Надоело управлять своими пользователями? Попробуйте API Okta и Java SDK сегодня. Аутентификация, управление и защита пользователей в любом приложении в течение нескольких минут.