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

Руководство по SAML с Spring Security

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

1. Обзор

В этом руководстве мы рассмотрим Spring Security SAML с Okta в качестве поставщика удостоверений (IdP) .

2. Что такое SAML?

Язык разметки подтверждения безопасности ( SAML ) — это открытый стандарт, который позволяет IdP безопасно отправлять данные аутентификации и авторизации пользователя поставщику услуг (SP) . Он использует сообщения на основе XML для связи между IdP и SP.

Другими словами, когда пользователь пытается получить доступ к службе, он должен войти в систему с IdP. После входа в систему IdP отправляет SP атрибуты SAML с данными авторизации и аутентификации в формате XML.

Помимо обеспечения защищенного механизма аутентификации-передачи, SAML также продвигает единый вход (SSO) , позволяя пользователям войти в систему один раз и повторно использовать одни и те же учетные данные для входа в систему других поставщиков услуг.

3. Настройка Окта SAML

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

3.1. Создать новое приложение

Затем мы создадим новую интеграцию веб-приложения с поддержкой SAML 2.0:

./97bfde9a2e26d976aceb9b0e071e4429.png

Далее мы заполним общую информацию, такую как название приложения и логотип приложения:

./7a1504e6fb86592a61450f5067e75f10.png

3.2. Изменить интеграцию SAML

На этом этапе мы предоставим параметры SAML, такие как URL-адрес единого входа и URI-адрес аудитории:

./4ba33a83a8374e0f0a1e4a7ec9aba870.png

Наконец, мы можем оставить отзыв о нашей интеграции:

./f93da598a27ea334b9cd8cf65c9a214b.png

3.3. Посмотреть инструкции по настройке

После завершения мы можем просмотреть инструкции по настройке нашего приложения Spring Boot:

./79348de90812c619eb4d6cfc73300cb5.png

Примечание. Мы должны скопировать инструкции, такие как URL-адрес издателя IdP и XML метаданных IdP, которые потребуются в дальнейшем в конфигурациях Spring Security:

./75b98cb20bfadede92a1a0c38b66bf54.png

4. Настройка весенней загрузки

Помимо обычных зависимостей Maven, таких как spring-boot-starter-web и spring-boot-starter-security , нам потребуется зависимость spring-security-saml2-core :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>

Кроме того, не забудьте добавить репозиторий Shibboleth для загрузки последней jar - файла opensaml , необходимой для зависимости spring-security-saml2-core :

<repository>
<id>Shibboleth</id>
<name>Shibboleth</name>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>

В качестве альтернативы мы можем настроить зависимости в проекте Gradle:

compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: "2.5.1" 
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: "2.5.1"
compile group: 'org.springframework.security.extensions', name: 'spring-security-saml2-core', version: "1.0.10.RELEASE"

5. Конфигурация безопасности Spring

Теперь, когда у нас есть готовый проект Okta SAML Setup и Spring Boot, давайте начнем с конфигураций Spring Security, необходимых для интеграции SAML 2.0 с Okta.

5.1. Точка входа SAML

Во-первых, мы создадим bean- компонент класса SAMLEntryPoint , который будет работать как точка входа для аутентификации SAML:

@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}

@Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}

Здесь bean-компонент WebSSOProfileOptions позволяет нам настроить параметры запроса, отправляемого от SP к IdP с запросом на аутентификацию пользователя.

5.2. Вход и выход

Далее давайте создадим несколько фильтров для наших SAML URI, таких как / discovery, / login и / logout :

@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlDiscovery()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter));
return new FilterChainProxy(chains);
}

Затем мы добавим несколько соответствующих фильтров и обработчиков:

@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}

@Bean
public SAMLDiscovery samlDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
return idpDiscovery;
}

@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/home");
return successRedirectHandler;
}

@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}

На данный момент мы настроили точку входа для аутентификации ( samlEntryPoint ) и несколько цепочек фильтров. Итак, давайте углубимся в их детали.

Когда пользователь пытается войти в систему в первый раз, samlEntryPoint обрабатывает запрос на вход. Затем bean-компонент samlDiscovery (если он включен) обнаружит IdP, с которым нужно связаться для аутентификации.

Затем, когда пользователь входит в систему, IdP перенаправляет ответ SAML на URI /saml/sso для обработки , и соответствующий samlWebSSOProcessingFilter будет аутентифицировать связанный маркер аутентификации.

В случае успеха SuccessRedirectHandler перенаправит пользователя на целевой URL-адрес по умолчанию ( /home ). В противном случае authenticationFailureHandler перенаправит пользователя на URL-адрес /error .

Наконец, давайте добавим обработчики выхода из системы для одиночного и глобального выхода:

@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl("/");
return successLogoutHandler;
}

@Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}

@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
}

@Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[] { logoutHandler() },
new LogoutHandler[] { logoutHandler() });
}

5.3. Обработка метаданных

Теперь мы предоставим SP метаданные IdP в формате XML. Это поможет сообщить нашему IdP, на какую конечную точку SP он должен перенаправляться после входа пользователя в систему.

Итак, мы настроим bean- компонент MetadataGenerator , чтобы Spring SAML мог обрабатывать метаданные:

public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(samlAudience);
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
return metadataGenerator;
}

@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}

@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
return extendedMetadata;
}

Компоненту MetadataGenerator требуется экземпляр KeyManager для шифрования обмена между SP и IdP:

@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource(samlKeystoreLocation);
Map<String, String> passwords = new HashMap<>();
passwords.put(samlKeystoreAlias, samlKeystorePassword);
return new JKSKeyManager(storeFile, samlKeystorePassword, passwords, samlKeystoreAlias);
}

Здесь мы должны создать и предоставить хранилище ключей компоненту KeyManager . Мы можем создать самоподписанный ключ и хранилище ключей с помощью команды JRE:

keytool -genkeypair -alias foreachspringsaml -keypass foreachsamlokta -keystore saml-keystore.jks

5.4. Менеджер метаданных

Затем мы настроим метаданные IdP в нашем приложении Spring Boot, используя экземпляр ExtendedMetadataDelegate :

@Bean
@Qualifier("okta")
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
org.opensaml.util.resource.Resource resource = null
try {
resource = new ClasspathResource("/saml/metadata/sso.xml");
} catch (ResourceException e) {
e.printStackTrace();
}
Timer timer = new Timer("saml-metadata")
ResourceBackedMetadataProvider provider = new ResourceBackedMetadataProvider(timer,resource);
provider.setParserPool(parserPool());
return new ExtendedMetadataDelegate(provider, extendedMetadata());
}

@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
List<MetadataProvider> providers = new ArrayList<>();
providers.add(oktaExtendedMetadataProvider());
CachingMetadataManager metadataManager = new CachingMetadataManager(providers);
metadataManager.setDefaultIDP(defaultIdp);
return metadataManager;
}

Здесь мы проанализировали метаданные из файла sso.xml , который содержит XML метаданных IdP, скопированных из учетной записи разработчика Okta при просмотре инструкций по установке.

Точно так же переменная defaultIdp содержит URL-адрес издателя IdP, скопированный из учетной записи разработчика Okta.

5.5. Разбор XML

Для разбора XML мы можем использовать экземпляр класса StaticBasicParserPool :

@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}

@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}

5.6. SAML-процессор

Затем нам требуется процессор для анализа сообщения SAML из HTTP-запроса:

@Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), VelocityFactory.getEngine());
}

@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}

@Bean
public SAMLProcessorImpl processor() {
ArrayList<SAMLBinding> bindings = new ArrayList<>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
return new SAMLProcessorImpl(bindings);
}

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

5.7. Реализация SAMLAuthenticationProvider

Наконец, нам требуется пользовательская реализация класса SAMLAuthenticationProvider для проверки экземпляра класса ExpiringUsernameAuthenticationToken и установки полученных полномочий:

public class CustomSAMLAuthenticationProvider extends SAMLAuthenticationProvider {
@Override
public Collection<? extends GrantedAuthority> getEntitlements(SAMLCredential credential, Object userDetail) {
if (userDetail instanceof ExpiringUsernameAuthenticationToken) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.addAll(((ExpiringUsernameAuthenticationToken) userDetail).getAuthorities());
return authorities;
} else {
return Collections.emptyList();
}
}
}

Кроме того, мы должны настроить CustomSAMLAuthenticationProvider как bean-компонент в классе SecurityConfig :

@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
return new CustomSAMLAuthenticationProvider();
}

5.8. SecurityConfig

Наконец, мы настроим базовую безопасность HTTP, используя уже рассмотренные samlEntryPoint и samlFilter :

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();

http.httpBasic().authenticationEntryPoint(samlEntryPoint);

http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);

http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated();

http
.logout()
.addLogoutHandler((request, response, authentication) -> {
response.sendRedirect("/saml/logout");
});
}

Вуаля! Мы завершили настройку Spring Security SAML, которая позволяет пользователю входить в IdP, а затем получать данные аутентификации пользователя в формате XML от IdP. Наконец, он аутентифицирует токен пользователя, чтобы разрешить доступ к нашему веб-приложению.

6. Домашний контроллер

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

6.1. Индекс и сопоставление аутентификации

Во-первых, давайте добавим сопоставления с целевым URI по умолчанию (/) и / auth URI:

@RequestMapping("/")
public String index() {
return "index";
}

@GetMapping(value = "/auth")
public String handleSamlAuth() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
return "redirect:/home";
} else {
return "/";
}
}

Затем мы добавим простой index.html , который позволит пользователю перенаправить аутентификацию Okta SAML, используя ссылку для входа :

<!doctype html>
<html>
<head>
<title>ForEach Spring Security SAML</title>
</head>
<body>
<h3><Strong>Welcome to ForEach Spring Security SAML</strong></h3>
<a th:href="@{/auth}">Login</a>
</body>
</html>

Теперь мы готовы запустить наше приложение Spring Boot и получить к нему доступ по адресу http://localhost:8080/ :

./1652f21b4dc207ff133bbd2da0a4716b.png

Страница входа в Okta должна открываться при нажатии на ссылку « Войти »:

./0e62513b3b7b76dec3742220125dca8e.png

6.2. Домашняя страница

Затем давайте добавим сопоставление с URI /home для перенаправления пользователя при успешной аутентификации:

@RequestMapping("/home")
public String home(Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("username", authentication.getPrincipal());
return "home";
}

Кроме того, мы добавим home.html , чтобы показать вошедшего в систему пользователя и ссылку для выхода:

<!doctype html>
<html>
<head>
<title>ForEach Spring Security SAML: Home</title>
</head>
<body>
<h3><Strong>Welcome!</strong><br/>You are successfully logged in!</h3>
<p>You are logged as <span th:text="${username}">null</span>.</p>
<small>
<a th:href="@{/logout}">Logout</a>
</small>
</body>
</html>

После успешного входа в систему мы должны увидеть домашнюю страницу:

./326aef62e5fa01b9fc6a024e8843afc0.png

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

В этом руководстве мы обсудили интеграцию Spring Security SAML с Okta.

Во-первых, мы настроили учетную запись разработчика Okta с веб-интеграцией SAML 2.0. Затем мы создали проект Spring Boot с необходимыми зависимостями Maven.

Затем мы выполнили все необходимые настройки для Spring Security SAML, такие как samlEntryPoint , samlFilter , обработка метаданных и процессор SAML .

Наконец, мы создали контроллер и несколько страниц, таких как index и home , чтобы протестировать интеграцию SAML с Okta.

Как обычно, исходный код доступен на GitHub .