Друзья не позволяют друзьям писать аутентификацию пользователя. Надоело управлять своими пользователями? Попробуйте 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 сегодня. Аутентификация, управление и защита пользователей в любом приложении в течение нескольких минут.