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:
Далее мы заполним общую информацию, такую как название приложения и логотип приложения:
3.2. Изменить интеграцию SAML
На этом этапе мы предоставим параметры SAML, такие как URL-адрес единого входа и URI-адрес аудитории:
Наконец, мы можем оставить отзыв о нашей интеграции:
3.3. Посмотреть инструкции по настройке
После завершения мы можем просмотреть инструкции по настройке нашего приложения Spring Boot:
Примечание. Мы должны скопировать инструкции, такие как URL-адрес издателя IdP и XML метаданных IdP, которые потребуются в дальнейшем в конфигурациях Spring Security:
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/ :
Страница входа в Okta должна открываться при нажатии на ссылку « Войти »:
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>
После успешного входа в систему мы должны увидеть домашнюю страницу:
7. Заключение
В этом руководстве мы обсудили интеграцию Spring Security SAML с Okta.
Во-первых, мы настроили учетную запись разработчика Okta с веб-интеграцией SAML 2.0. Затем мы создали проект Spring Boot с необходимыми зависимостями Maven.
Затем мы выполнили все необходимые настройки для Spring Security SAML, такие как samlEntryPoint
, samlFilter
, обработка метаданных и процессор SAML .
Наконец, мы создали контроллер и несколько страниц, таких как index
и home
, чтобы протестировать интеграцию SAML с Okta.
Как обычно, исходный код доступен на GitHub .