Перейти к основному содержимому

Spring Security и OpenID Connect

· 12 мин. чтения

Обратите внимание, что эта статья была обновлена до нового стека Spring Security OAuth 2.0. Тем не менее, учебник с использованием устаревшего стека все еще доступен.

1. Обзор

В этом кратком руководстве мы сосредоточимся на настройке OpenID Connect (OIDC) с помощью Spring Security.

Мы представим различные аспекты этой спецификации, а затем увидим поддержку, которую Spring Security предлагает для ее реализации на клиенте OAuth 2.0.

2. Краткое введение в OpenID Connect

OpenID Connect — это уровень идентификации, созданный поверх протокола OAuth 2.0.

Таким образом, очень важно знать OAuth 2.0 , прежде чем углубляться в OIDC, особенно в поток кода авторизации.

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

  • Ядро: аутентификация и использование утверждений для передачи информации о конечном пользователе.
  • Обнаружение: определяет, как клиент может динамически получать информацию о провайдерах OpenID.
  • Динамическая регистрация: определяет, как клиент может зарегистрироваться у провайдера.
  • Управление сеансом: определяет, как управлять сеансами OIDC.

Кроме того, в документах различают серверы аутентификации OAuth 2.0, которые предлагают поддержку этой спецификации, называя их «поставщиками OpenID» (OP) и клиентами OAuth 2.0, которые используют OIDC в качестве проверяющих сторон (RP). Мы будем придерживаться этой терминологии в этой статье.

Стоит также знать, что клиент может запросить использование этого расширения, добавив область openid в свой запрос на авторизацию.

Наконец, еще один аспект, который полезно понять для этого руководства, заключается в том, что OP выдают информацию о конечном пользователе в виде JWT, называемого «токен идентификатора».

Теперь да, мы готовы погрузиться глубже в мир OIDC.

3. Настройка проекта

Прежде чем сосредоточиться на фактической разработке, нам нужно зарегистрировать клиент OAuth 2.o у нашего поставщика OpenID.

В этом случае мы будем использовать Google в качестве поставщика OpenID. Мы можем следовать этим инструкциям , чтобы зарегистрировать наше клиентское приложение на их платформе. Обратите внимание, что область openid присутствует по умолчанию.

URI перенаправления, который мы настроили в этом процессе, является конечной точкой в нашей службе: http://localhost:8081/login/oauth2/code/google.

Мы должны получить Client Id и Client Secret из этого процесса.

3.1. Конфигурация Maven

Мы начнем с добавления этих зависимостей в файл pom нашего проекта:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>

Стартовый артефакт объединяет все зависимости, связанные с Spring Security Client, включая:

  • зависимость spring-security-oauth2-client для входа OAuth 2.0 и функций клиента
  • библиотека JOSE для поддержки JWT

Как обычно, последнюю версию этого артефакта мы можем найти с помощью поисковой системы Maven Central .

4. Базовая конфигурация с использованием Spring Boot

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

Использование Spring Boot делает это очень просто, так как все, что нам нужно сделать, это определить два свойства приложения:

spring:
security:
oauth2:
client:
registration:
google:
client-id: <client-id>
client-secret: <secret>

Давайте запустим наше приложение и попробуем получить доступ к конечной точке. Мы увидим, что нас перенаправляют на страницу входа в Google для нашего клиента OAuth 2.0.

Это выглядит очень просто, но под капотом здесь происходит довольно много вещей. Далее мы рассмотрим, как Spring Security справляется с этим.

Ранее, в нашем сообщении о поддержке WebClient и OAuth 2 , мы анализировали внутренности того, как Spring Security обрабатывает серверы и клиенты авторизации OAuth 2.0.

Там мы увидели, что нам нужно предоставить дополнительные данные, помимо идентификатора клиента и секрета клиента, для успешной настройки экземпляра ClientRegistration . Итак, как это работает?

Ответ заключается в том, что Google — известный провайдер, поэтому фреймворк предлагает некоторые предопределенные свойства для упрощения работы.

Мы можем взглянуть на эти конфигурации в перечислении CommonOAuth2Provider .

Для Google перечисляемый тип определяет такие свойства, как:

  • области действия по умолчанию, которые будут использоваться
  • конечная точка авторизации
  • конечная точка токена
  • конечная точка UserInfo, которая также является частью спецификации OIDC Core.

4.1. Доступ к информации о пользователе

Spring Security предлагает полезное представление участника-пользователя, зарегистрированного у поставщика OIDC, объекта OidcUser .

Помимо основных методов OAuth2AuthenticatedPrincipal , этот объект предлагает некоторые полезные функции:

  • получить значение ID Token и содержащиеся в нем утверждения
  • получить утверждения, предоставленные конечной точкой UserInfo
  • генерировать совокупность двух наборов

Мы можем легко получить доступ к этому объекту в контроллере:

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
@AuthenticationPrincipal OidcUser principal) {
return principal;
}

Или с помощью SecurityContextHolder в bean-компоненте:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
OidcUser principal = ((OidcUser) authentication.getPrincipal());

// ...
}

Если мы проверим принципала, мы увидим здесь много полезной информации, такой как имя пользователя, адрес электронной почты, изображение профиля и локаль.

Кроме того, важно отметить, что Spring добавляет полномочия к принципалу на основе областей, полученных от провайдера, с префиксом « SCOPE_ ». Например, область действия openid становится предоставленным полномочием SCOPE_openid .

Эти полномочия могут использоваться для ограничения доступа к определенным ресурсам, например :

@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/my-endpoint")
.hasAuthority("SCOPE_openid")
.anyRequest().authenticated()
);
}
}

5. OIDC в действии

До сих пор мы узнали, как мы можем легко реализовать решение для входа в OIDC с помощью Spring Security.

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

Но правда в том, что до сих пор нам не приходилось иметь дело с какими-либо специфическими аспектами OIDC. Это означает, что Spring делает большую часть работы за нас.

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

5.1. Процесс входа

Чтобы увидеть это ясно, давайте включим журналы RestTemplate , чтобы видеть запросы, которые выполняет служба:

logging:
level:
org.springframework.web.client.RestTemplate: DEBUG

Если мы сейчас вызовем защищенную конечную точку, мы увидим, что служба выполняет обычный поток кода авторизации OAuth 2.0. Это потому, что, как мы уже говорили, эта спецификация построена на основе OAuth 2.0. Во всяком случае, некоторые отличия есть.

Во-первых, в зависимости от используемого нами провайдера и настроенных областей мы можем увидеть, что служба выполняет вызов конечной точки UserInfo, о которой мы упоминали в начале.

А именно, если ответ авторизации извлекает хотя бы один из профилей , адресов электронной почты , адресов или телефонов , платформа вызовет конечную точку UserInfo для получения дополнительной информации.

Несмотря на то, что все указывает на то, что Google должен получить профиль и область электронной почты — поскольку мы используем их в запросе авторизации — OP вместо этого извлекает их пользовательские аналоги, https://www.googleapis.com/auth/userinfo.email и https://www.googleapis.com/auth/userinfo.profile , поэтому Spring не вызывает конечную точку.

Это означает, что вся информация, которую мы получаем, является частью идентификатора токена.

Мы можем адаптироваться к этому поведению, создав и предоставив собственный экземпляр OidcUserService :

@Configuration
public class OAuth2LoginSecurityConfig
extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
Set<String> googleScopes = new HashSet<>();
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.email");
googleScopes.add(
"https://www.googleapis.com/auth/userinfo.profile");

OidcUserService googleUserService = new OidcUserService();
googleUserService.setAccessibleScopes(googleScopes);

http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin
.userInfoEndpoint()
.oidcUserService(googleUserService));
}
}

Второе отличие, которое мы заметим, — это вызов JWK Set URI. Как мы объяснили в нашей публикации JWS и JWK , это используется для проверки подписи токена идентификатора в формате JWT.

Далее мы подробно проанализируем ID Token.

5.2. Идентификационный токен

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

Как мы уже говорили ранее, объект OidcUser содержит утверждения, содержащиеся в токене идентификатора, и фактический токен в формате JWT , который можно проверить с помощью jwt.io.

Вдобавок к этому Spring предлагает множество удобных геттеров для простого получения стандартных утверждений, определенных спецификацией.

Мы видим, что ID Token включает в себя некоторые обязательные утверждения:

  • идентификатор эмитента в формате URL (например, « https://accounts.google.com »)
  • идентификатор субъекта, который является ссылкой на конечного пользователя, содержащейся эмитентом
  • срок действия токена
  • время выпуска токена
  • аудитория, которая будет содержать настроенный нами идентификатор клиента OAuth 2.0

А также многие стандартные претензии ODDC , подобные тем, которые мы упоминали ранее ( имя , локаль , изображение , электронная почта ).

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

5.3. Претензии и области применения

Как мы можем себе представить, утверждения, извлекаемые OP, соответствуют областям, которые мы (или Spring Security) настроили.

OIDC определяет некоторые области, которые можно использовать для запроса утверждений, определенных OIDC:

  • profile , который можно использовать для запроса утверждений профиля по умолчанию (например , имя, предпочитаемое_имя_пользователя, изображение и т. д. )
  • email , чтобы получить доступ к электронной почте и email_verified Claims
  • адрес
  • phone , чтобы запросить претензии phone_number и phone_number_verified

Несмотря на то, что Spring еще не поддерживает его, спецификация позволяет запрашивать отдельные претензии, указывая их в запросе авторизации.

6. Поддержка Spring для обнаружения OIDC

Как мы объяснили во введении, OIDC включает в себя множество различных функций, помимо своей основной цели.

Возможности, которые мы собираемся проанализировать в этом и следующем разделах, являются необязательными в OIDC. Следовательно, важно понимать, что могут быть OP, которые их не поддерживают.

Спецификация определяет механизм обнаружения для RP для обнаружения OP и получения информации, необходимой для взаимодействия с ним.

Короче говоря, OP предоставляют документ JSON со стандартными метаданными. Информация должна передаваться известной конечной точкой расположения эмитента, /.well-known/openid-configuration .

Spring выигрывает от этого, позволяя нам настроить ClientRegistration только с одним простым свойством, местоположением эмитента.

Но давайте сразу перейдем к примеру, чтобы ясно это увидеть.

Мы определим пользовательский экземпляр ClientRegistration :

spring:
security:
oauth2:
client:
registration:
custom-google:
client-id: <client-id>
client-secret: <secret>
provider:
custom-google:
issuer-uri: https://accounts.google.com

Теперь мы можем перезапустить наше приложение и проверить журналы, чтобы убедиться, что приложение вызывает конечную точку конфигурации openid в процессе запуска.

Мы даже можем просмотреть эту конечную точку, чтобы просмотреть информацию, предоставленную Google:

https://accounts.google.com/.well-known/openid-конфигурация

Мы можем видеть, например, конечные точки авторизации, токена и пользовательской информации, которые должна использовать служба, а также поддерживаемые области.

Особенно важным примечанием здесь является тот факт, что если конечная точка обнаружения недоступна во время запуска службы, наше приложение не сможет успешно завершить процесс запуска.

7. Управление сессиями OpenID Connect

Эта спецификация дополняет основные функциональные возможности, определяя:

  • различные способы постоянного мониторинга статуса входа конечного пользователя в OP, чтобы RP могла выйти из системы конечного пользователя, который вышел из провайдера OpenID
  • возможность регистрации URI выхода RP с OP как часть регистрации клиента, чтобы получать уведомления, когда конечный пользователь выходит из OP
  • механизм для уведомления OP о том, что конечный пользователь вышел с сайта и может также захотеть выйти из OP

Естественно, не все ОП поддерживают все эти пункты, а некоторые из этих решений можно реализовать только во фронтальной реализации через User-Agent.

В этом руководстве мы сосредоточимся на возможностях, предлагаемых Spring для последнего элемента списка, выхода из системы, инициированного RP.

На этом этапе, если мы войдем в наше приложение, мы обычно сможем получить доступ к каждой конечной точке.

Если мы выйдем из системы (вызвав конечную точку /logout ) и после этого сделаем запрос к защищенному ресурсу, мы увидим, что можем получить ответ без необходимости повторного входа в систему.

Однако на самом деле это не так; если мы проверим вкладку «Сеть» в консоли отладки браузера, мы увидим, что когда мы попадаем в защищенную конечную точку во второй раз, мы перенаправляемся в конечную точку авторизации OP, и, поскольку мы все еще вошли в систему, поток завершается прозрачно , почти мгновенно попадая в защищенную конечную точку.

Конечно, в некоторых случаях это может быть нежелательным. Давайте посмотрим, как мы можем реализовать этот механизм OIDC, чтобы справиться с этим.

7.1. Конфигурация провайдера OpenID

В этом случае мы будем настраивать и использовать экземпляр Okta в качестве нашего поставщика OpenID. Мы не будем вдаваться в подробности того, как создать экземпляр, но мы можем следовать шагам этого руководства и помнить, что конечной точкой обратного вызова Spring Security по умолчанию будет /login/oauth2/code/okta.

В нашем приложении мы можем определить регистрационные данные клиента со свойствами:

spring:
security:
oauth2:
client:
registration:
okta:
client-id: <client-id>
client-secret: <secret>
provider:
okta:
issuer-uri: https://dev-123.okta.com

OIDC указывает, что конечная точка выхода из системы OP может быть указана в документе обнаружения как элемент end_session_endpoint .

7.2. Конфигурация LogoutSuccessHandler _

Далее нам нужно настроить логику выхода HttpSecurity , предоставив настраиваемый экземпляр LogoutSuccessHandler :

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/home").permitAll()
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin.permitAll())
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler()));
}

Теперь давайте посмотрим, как мы можем создать LogoutSuccessHandler для этой цели, используя специальный класс, предоставленный Spring Security, OidcClientInitiatedLogoutSuccessHandler:

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(
this.clientRegistrationRepository);

oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
URI.create("http://localhost:8081/home"));

return oidcLogoutSuccessHandler;
}

Следовательно, нам нужно настроить этот URI как допустимый URI перенаправления выхода из системы на панели конфигурации клиента OP.

Очевидно, что конфигурация выхода из системы OP содержится в настройке регистрации клиента, поскольку все, что мы используем для настройки обработчика, — это bean-компонент ClientRegistrationRepository , присутствующий в контексте.

Итак, что теперь будет?

После входа в наше приложение мы можем отправить запрос на конечную точку /logout , предоставленную Spring Security.

Если мы проверим сетевые журналы в консоли отладки браузера, мы увидим, что нас перенаправили на конечную точку выхода из системы OP, прежде чем, наконец, мы получили доступ к настроенному нами URI перенаправления.

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

8. Заключение

Подводя итог, в этом руководстве мы многое узнали о решениях, предлагаемых OpenID Connect, и о том, как мы можем реализовать некоторые из них с помощью Spring Security.

Как всегда, все полные примеры можно найти в нашем репозитории GitHub .