SAML2 ещё жив?! Как интегрировать Keycloak со Spring Boot в 2025 году

от автора

Команда Spring АйО перевела статью о совместном использовании Spring Boot, SAML2 и Keycloak. В статье также приводятся некоторые кастомизированные решения, позволяющие более гибко работать с упомянутым набором технологий.


Эта статья расскажет вам, как использовать аутентификацию SAML2 со Spring Boot и Keycloak. Security Assertion Markup Language (SAML) — это стандартное решения для обмена аутентификационными и авторизационными идентификаторами между провайдером идентификатора (IdP) и провайдером сервиса. Это основанный на XML протокол, который использует токены безопасности с информацией о Principal. В настоящее время он менее популярен, чем OICD (OpenID Connect), но пока не является устаревшим. По факту, многие организации все еще используют SAML для SSO.

В нашем примере Keycloak будет действовать как провайдер идентификатора. Keycloak поддерживает SAML 2.0. Мы также можем использовать механизмы Spring Security, поддерживающие аутентификацию SAML на стороне сервис провайдера (наше приложение-пример на Spring Boot). Существует несколько статей про Spring Boot и SAML, но лишь немногие из них соответствуют актуальному состоянию дел и используют Keycloak как IdP.

Исходный код

Если вы хотите попробовать выполнить это упражнение самостоятельно, вы можете посмотреть на исходный код. Прежде всего, вам необходимо клонировать следующий GitHub репозиторий. Он содержит несколько примеров приложений на Java для демонстрации работы с безопасностью в Spring Boot. Чтобы продолжить работу с упражнением, вам следует пойти в каталог saml. Приложение-пример на Spring Boot находится в каталоге callme-saml. Далее следуйте инструкциям. 

Предварительная подготовка 

Прежде чем начинать разработку, мы должны установить кое-какие инструменты на наши ноутбуки. Конечно, нам понадобится Maven и как минимум Java 17 (а лучше Java 21). Нам также понадобится доступ к ПО для контейнеризации приложений, например,  Docker или Podman для запуска инстанса Keycloak.

Run Keycloak

Мы будем запускать Keycloak как Docker-контейнер. Репозиторий содержит файл docker-compose.yml в каталоге saml и realm manifest в каталоге saml/config. Docker Compose запускает Keycloak в режиме разработки и импортирует realm файл при запуске. Благодаря этому, вам не придется самостоятельно создавать многочисленные ресурсы в Keycloak. Однако, я пошагово опишу, что необходимо сделать. Манифест docker-compose.yml также устанавливает пароль админа по умолчанию в admin, включает HTTPS и использует сборку Red Hat для Keycloak вместо community edition.

services:   keycloak: #image: quay.io/keycloak/keycloak:24.0 image: registry.redhat.io/rhbk/keycloak-rhel9:24-17 environment:   - KEYCLOAK_ADMIN=admin   - KEYCLOAK_ADMIN_PASSWORD=admin   - KC_HTTP_ENABLED=true   - KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/localhost.key.pem   - KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/localhost.crt.pem   - KC_HTTPS_TRUST_STORE_FILE=/opt/keycloak/conf/truststore.jks   - KC_HTTPS_TRUST_STORE_PASSWORD=123456 ports:   - 8080:8080   - 8443:8443 volumes:   - /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/localhost-key.pem:/opt/keycloak/conf/localhost.key.pem   - /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/localhost-crt.pem:/opt/keycloak/conf/localhost.crt.pem   - /Users/pminkows/IdeaProjects/sample-spring-security-microservices/oauth/truststore.jks:/opt/keycloak/conf/truststore.jks   - ./config/:/opt/keycloak/data/import:ro command: start-dev --import-realm

В самом конце нам необходимо выполнить вот эту команду, чтобы запустить инстанс Keycloak.

docker compose up

Далее, нам необходим работающий инстанс Keycloak, доступный на localhost по HTTPS порту 8443.

spring-boot-saml2-keycloak-startup

spring-boot-saml2-keycloak-startup

Когда вы войдете в интерфейс Keycloak с логином и паролем admin / admin, вы должны увидеть realm spring-boot-keycloak.

В подробном описании realm spring-boot-keycloak присутствует ссылка на файл метаданных для провайдера SAML2. Мы можем скачать этот файл и передать его напрямую в приложение на Spring Boot, чтобы оно сохранило адрес этой ссылки для будущего использования. Keycloak опубликовал метаданные IdP для realm spring-boot-keycloak по адресу https://localhost:8443/realms/spring-boot-keycloak/protocol/saml/descriptor. Давайте скопируем этот адрес в буфер обмена и продолжим реализацию приложения на Spring Boot.

spring-boot-saml2-idp

spring-boot-saml2-idp

Создание приложения на Spring Boot с поддержкой SAML2 

Давайте начнем со списка зависимостей. Мы должны включить модули Spring Web и Spring Security. Для поддержки SAML2 необходимо добавить зависимость spring-security-saml2-service-provider. Эта зависимость использует библиотеку OpenSAML, опубликованную в выделенном Shibboleth Maven репозитории. Нашему приложению также требуется Thymeleaf, чтобы создать единственную веб страницу с данными прошедшего аутентификацию Principal-а.

<dependencies>   <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>   </dependency>   <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-security</artifactId>   </dependency>   <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-thymeleaf</artifactId>   </dependency>   <dependency>  <groupId>org.thymeleaf.extras</groupId>  <artifactId>thymeleaf-extras-springsecurity6</artifactId>   </dependency>   <dependency>  <groupId>org.springframework.security</groupId>  <artifactId>spring-security-saml2-service-provider</artifactId>   </dependency> </dependencies>  <repositories>   <repository> <id>shibboleth-build-releases</id> <name>Shibboleth Build Releases Repository</name> <url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>   </repository> </repositories>

Наша цель состоит в том, чтобы начать с чего-то базового. Приложение опубликует эндпоинт метаданных, используя DSL метод saml2Metadata. Мы также включаем авторизацию на уровне методов с аннотацией @EnableMethodSecurity. Там мы сможем получать доступ к ресурсам после аутентификации в Keycloak.

@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig {  @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {     http.csrf(AbstractHttpConfigurer::disable)             .authorizeHttpRequests(authorize -> authorize.anyRequest()                     .authenticated())             .saml2Login(withDefaults())             .saml2Metadata(withDefaults());     return http.build(); }  } 

Далее приводится содержимое файла  application.yml для Spring Boot. Он меняет HTTP порт по умолчанию на 8081. Конфигурация SAML2 должна содержать свойства spring.security.saml2.relyingparty.registration.{registrationId}.*. Необходимо задать адрес файла метаданных IdP через свойство assertingparty.metadata-uri. Нам также необходимо задать entity-id и адрес SSO сервиса. Оба эти свойства доступны через файл метаданных IdP. Мы также должны предоставить сертификаты для подписи запросов и проверки ответов от SAML. Ключ и сертификат уже присутствуют в репозитории.

server.port: 8081  spring:   security: saml2:   relyingparty:     registration:       keycloak:         identityprovider:           entity-id: https://localhost:8443/realms/spring-boot-keycloak           verification.credentials:             - certificate-location: classpath:rp-certificate.crt           singlesignon.url: https://localhost:8443/realms/spring-boot-keycloak/protocol/saml           singlesignon.sign-request: false         signing:           credentials:             - private-key-location: classpath:rp-key.key               certificate-location: classpath:rp-certificate.crt         assertingparty:           metadata-uri: https://localhost:8443/realms/spring-boot-keycloak/protocol/saml/descriptor

Давайте запустим наше приложение при помощи следующей команды:

mvn spring-boot:run

После запуска мы можем показать эндпоинт метаданных saml2Metadata, доступный через DSL. Самый важный элемент в файле — это entityID. Давайте сохраним его для будущего использования.

<md:EntityDescriptor entityID="http://localhost:8081/saml2/service-provider-metadata/keycloak">   <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:KeyDescriptor use="signing">    <ds:KeyInfo>       <ds:X509Data>       <ds:X509Certificate>...</ds:X509Certificate>       </ds:X509Data>    </ds:KeyInfo> </md:KeyDescriptor> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/login/saml2/sso/keycloak" index="1"/>   </md:SPSSODescriptor> </md:EntityDescriptor>

Конфигурируем клиент Keycloak SAML2 

Давайте переключимся на пользовательский интерфейс Keycloak. Мы должны создать SAML клиент. ID клиента должен быть тем же самым, что и entityID, полученный в предыдущем разделе. Мы также должны установить корневой URL для приложения и валидные URI для редиректов. Отметим, что вам ничего не надо делать:  Keycloak импортируем все требуемые конфигурации при запуске из экспортируемого манифеста realm.

spring-boot-saml2-client

spring-boot-saml2-client

Не забудьте создать тестового пользователя с паролем. Мы будем использовать его для аутентификации в Keycloak на следующем шаге:

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

Давайте включим только что созданный scope к другим scope-ам клиента.

Мы также можем создать группу admins и добавить нашего тестового пользователя в эту группу.

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

@Controller public class MainController {  @GetMapping("/") public String getPrincipal(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {     String emailAddress = principal.getFirstAttribute("email");     model.addAttribute("emailAddress", emailAddress);     model.addAttribute("userAttributes", principal.getAttributes());     return "index"; }  }

Вот главный сайт приложения. Он использует расширение Thymeleaf для Spring Security.

<!doctype html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"   xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">  <head> <title>SAML 2.0 Login</title> </head>  <body> <main role="main" class="container">     <h1 class="mt-5">SAML 2.0 Login with Spring Security</h1>     <p class="lead">You are successfully logged in as <span sec:authentication="name"></span></p>     <h2 class="mt-2">User Identity Attributes</h2>     <table class='table table-striped'>         <thead>         <tr>             <th>Attribute</th>             <th>Value</th>         </tr>         </thead>         <tbody>         <tr th:each="userAttribute : ${userAttributes}">             <th th:text="${userAttribute.key}"></th>             <td th:text="${userAttribute.value}"></td>         </tr>         </tbody>     </table> </main> </body>  </html> 

Когда мы заходим на главный сайт приложения по URL http://localhost:8081, Spring должен перенаправить нас на сайт логина Keycloak. Давайте введем логин и пароль нашего тестового пользователя.

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

spring-boot-saml2-auth

spring-boot-saml2-auth

Авторизация через методы REST 

Давайте посмотрим на следующий @RestController в нашем приложении. Мы уже включили авторизацию на уровне метода через аннотацию @EnableMethodSecurity. Наш контроллер создает два REST эндпоинта. Эндпоинт GET /greetings/user требует, чтобы ему предоставили права ROLE_USER, а для GET /greetings/admin требуются права ROLE_ADMINS.

@RestController @RequestMapping("/greetings") public class GreetingController {  @GetMapping("/user") @PreAuthorize("hasAuthority('ROLE_USER')") public String greeting() {     return "I'm SAML user!"; }  @GetMapping("/admin") @PreAuthorize("hasAuthority('ROLE_ADMINS')") public String admin() {     return "I'm SAML admin!"; }  }

Давайте вызовем эндпоинт GET /greetings/user. Он возвращает ожидаемый ответ.

Теперь давайте вызовем эндпоинт GET /greetings/admin. К сожалению, у нас нет доступа к этому эндпоинту. Это связано с тем, что он требует, чтобы у пользователя были права, как у роли ROLE_ADMINS.

Ранее мы использовали реализацию провайдера аутентификации SAML2 по умолчанию bp Spring Boot. Он устанавливает только ROLE_USER как назначаемую роль. Мы можем переопределить это поведение с помощью метода setResponseAuthenticationConverter реализации SAML2 AuthenticationProvider. Вот его текущая реализация. Наш метод пытается найти необходимый атрибут в токене ответа SAML2. Затем он сопоставляет имя группы, взятое из этого атрибута, с именем Spring Security GrantedAuthority. И в конце он возвращает новый объект класса Saml2Authentication с новым списком объектов GrantedAuthority.

@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig {      private static final Logger LOG = LoggerFactory         .getLogger(SecurityConfig.class);      @Bean     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {          OpenSaml4AuthenticationProvider provider =              new OpenSaml4AuthenticationProvider();          provider.setResponseAuthenticationConverter(token -> {             var auth = OpenSaml4AuthenticationProvider                 .createDefaultResponseAuthenticationConverter()                 .convert(token);             LOG.info("AUTHORITIES: {}", auth.getAuthorities());              var attrValues = token.getResponse().getAssertions().stream()                     .flatMap(as -> as.getAttributeStatements().stream())                     .flatMap(attrs -> attrs.getAttributes().stream())                         .filter(attrs -> attrs.getName().equals("member"))                         .findFirst().orElseThrow().getAttributeValues();              if (!attrValues.isEmpty()) {                 var member = ((XSStringImpl) attrValues.getFirst()).getValue();                 LOG.info("MEMBER: {}", member);                 List<GrantedAuthority> authoritiesList = List.of(                         new SimpleGrantedAuthority("ROLE_USER"),                         new SimpleGrantedAuthority("ROLE_" +                              member.toUpperCase().replaceFirst("/", ""))                 );                  LOG.info("NEW AUTHORITIES: {}", authoritiesList);                 return new Saml2Authentication(                     (AuthenticatedPrincipal) auth.getPrincipal(),                      auth.getSaml2Response(),                      authoritiesList);             } else return auth;         });          http.csrf(AbstractHttpConfigurer::disable)                 .authorizeHttpRequests(authorize -> authorize.anyRequest()                         .authenticated())                 .saml2Login(saml2 -> saml2                         .authenticationManager(new ProviderManager(provider))                 )                 .saml2Metadata(withDefaults());         return http.build();     }  }

Давайте запустим новую версию нашего приложения.

mvn spring-boot:run

Теперь, когда мы снова откроем сайт http://localhost:8081, мы сможем посмотреть на логи. На скриншоте выделена та часть ответа SAML, которая содержит атрибут участника. Как вы видите в нижней части скриншота, мы создали новый список выданных ролей, содержащий ROLE_USER и ROLE_ADMINS.

Теперь мы наконец-то можем снова открыть эндпоинт GET /greetings/admin. Он работает!

spring-boot-saml2-admin

spring-boot-saml2-admin

Эта статья показывает, как максимально просто запустить приложение с использованием Spring Boot, SAML2 и Keycloak. Она также предоставляет более совершенное решение с кастомизированной реализацией провайдера аутентификации, которая сопоставляет имя группы пользователей с выданной ролью. Хотя SAML2 является довольно зрелой технологией, большого количества примеров совместного использования Spring Boot, SAML2 и Keycloak не существует. Я надеюсь, что моя статья восполнит этот пробел. 🙂


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.


ссылка на оригинал статьи https://habr.com/ru/articles/895022/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *