Spring Security的SAML指南
- Spring
- Spring Security
- SAML
1.概述
在本教程中,我們將使用Okta作為身份提供者(IdP)探索Spring Security SAML。
2.什麼是SAML?
安全聲明標記語言(SAML)是一種開放標準,允許IdP將用戶的身份驗證和授權詳細信息安全地發送到服務提供商(SP) 。它使用基於XML的消息進行IdP和SP之間的通信。
換句話說,當用戶嘗試訪問服務時,要求他使用IdP登錄。登錄後, IdP將帶有XML格式的授權和身份驗證詳細信息的SAML屬性發送到SP。
除了提供安全的身份驗證傳輸機制外, SAML還促進了單一登錄(SSO) ,允許用戶登錄一次並重複使用相同的憑據登錄其他服務提供商。
3. Okta SAML設置
首先,作為先決條件,我們應該設置一個Okta開發人員帳戶。
3.1。創建新的應用程序
然後,我們將創建一個具有SAML 2.0支持的新Web應用程序集成:
接下來,我們將填寫常規信息,例如App名稱和App徽標:
3.2。編輯SAML集成
在此步驟中,我們將提供SAML設置,例如SSO URL和Audience URI:
最後,我們可以提供有關集成的反饋:
3.3。查看安裝說明
完成後,我們可以查看我們的Spring Boot App的設置說明:
注意:我們應該複製IdP發行者URL和IdP元數據XML之類的說明,這些要求在Spring Security配置中將進一步需要:
4. Spring Boot設置
除了通常的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.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
另外,請確保添加Shibboleth
存儲庫以下載spring-security-saml2-core
依賴項所需**opensaml
jar:**
<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.4.2"
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: "2.4.2"
compile group: 'org.springframework.security.extensions', name: 'spring-security-saml2-core', version: "1.0.10.RELEASE"
5. Spring安全配置
現在我們已經準備好Okta SAML安裝程序和Spring Boot項目,讓我們從與Okta集成SAML 2.0所需的Spring Security配置開始。
5.1 SAML入口點
首先,我們將創建SAMLEntryPoint
類的bean,它將用作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;
}
在這裡, WebSSOProfileOptions
bean使我們可以設置從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
將處理輸入請求。然後, samlDiscovery
bean(如果啟用)將發現要聯繫以進行身份驗證的IdP。
接下來,當用戶登錄時, IdP將SAML響應重定向到/saml/sso
URI進行處理,並且相應的samlWebSSOProcessingFilter
將對關聯的身份驗證令牌進行身份驗證。
成功後, successRedirectHandler
會將用戶重定向到默認目標URL( /home
)。否則, authenticationFailureHandler
會將用戶重定向到/error
URL。
最後,讓我們為單個和全局註銷添加註銷處理程序:
@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端點將很有幫助。
因此,我們將配置MetadataGenerator
bean來啟用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
bean需要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
bean。我們可以使用JRE命令創建一個自簽名密鑰和密鑰庫:
keytool -genkeypair -alias baeldungspringsaml -keypass baeldungsamlokta -keystore saml-keystore.jks
5.4 MetadataManager
ExtendedMetadataDelegate
實例將IdP元數據配置到我們的Spring Boot應用程序中:
@Bean
@Qualifier("okta")
public ExtendedMetadataDelegate oktaExtendedMetadataProvider() throws MetadataProviderException {
File metadata = null;
try {
metadata = new File("./src/main/resources/saml/metadata/sso.xml");
} catch (Exception e) {
e.printStackTrace();
}
FilesystemMetadataProvider provider = new FilesystemMetadataProvider(metadata);
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
文件中解析了包含IdP元數據XML的元數據,該文件是在查看設置說明時從Okta開發人員帳戶複製的。
同樣, defaultIdp
變量包含從Okta開發人員帳戶複製的IdP頒發者URL。
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處理器
然後,我們需要處理器從HTTP請求中解析SAML消息:
@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);
}
在這裡,針對Okta開發人員帳戶中的配置,我們使用了POST和重定向綁定。
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();
}
}
}
另外,我們應該SecurityConfig
類CustomSAMLAuthenticationProvider
配置為Bean:
@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
return new CustomSAMLAuthenticationProvider();
}
5.8 SecurityConfig
最後,我們將使用已經討論過的samlEntryPoint
和samlFilter
配置基本的HTTP安全性:
@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,然後從IdP接收XML格式的用戶身份驗證詳細信息。最後,它對用戶令牌進行身份驗證,以允許訪問我們的Web應用程序。
6. HomeController
現在我們已經準備好了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
,它允許用戶使用login
鏈接重定向Okta SAML身份驗證:
<!doctype html>
<html>
<head>
<title>Baeldung Spring Security SAML</title>
</head>
<body>
<h3><Strong>Welcome to Baeldung Spring Security SAML</strong></h3>
<a th:href="@{/auth}">Login</a>
</body>
</html>
現在,我們準備運行我們的Spring Boot App並通過http:// localhost:8080 /進行訪問:
Login
鏈接時,應打開“ Okta登錄”頁面:
6.2 主頁
接下來,讓我們將映射添加到/home
URI,以在成功通過身份驗證後重定向用戶:
@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>Baeldung 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的集成。
首先,我們使用SAML 2.0 Web集成設置了Okta開發人員帳戶。然後,我們創建了一個具有必需的Maven依賴項的Spring Boot項目。
接下來,我們完成了Spring Security SAML的samlEntryPoint
, samlFilter
,元數據處理和SAML處理器。
最後,我們創建了一個控制器以及一些頁面(如index
和home
來測試我們與Okta的SAML集成。