1. 개요

이 예제에서는 Okta를 자격 증명 Provider(IdP)로 사용하는 Spring Security SAML살펴보겠습니다 .

2. SAML이란 무엇입니까?

SAML (Security Assertion Markup Language )은 IdP가 사용자의 인증 및 승인 세부 정보를 SP(서비스 Provider)에게 안전하게 보낼 수 있도록 하는 개방형 표준입니다 . IdP와 SP 간의 통신을 위해 XML 기반 메시지를 사용합니다.

즉, 사용자가 서비스에 접근하려면 IdP로 로그인해야 합니다. 로그인 하면 IdP는 권한 부여 및 인증 세부 정보가 포함된 SAML 특성을 XML 형식으로 SP에 보냅니다.

Security 인증 전송 메커니즘을 제공하는 것 외에도 SAML은 SSO(Single Sign-On)를 촉진 하여 사용자가 한 번 로그인하고 동일한 자격 증명을 재사용하여 다른 서비스 Provider에 로그인할 수 있도록 합니다.

3. Okta SAML 설정

먼저 전제 조건으로 Okta 개발자 계정을 설정 해야 합니다 .

3.1. 새 애플리케이션 만들기

그런 다음 SAML 2.0을 지원하는 새로운 웹 애플리케이션 통합을 생성합니다.

Screen-Shot-2021-02-19-at-4.00.09-PM

다음으로 앱 이름 및 앱 로고와 같은 일반 정보를 입력합니다.

Screen-Shot-2021-02-19-at-4.02.25-PM

3.2. SAML 통합 편집

이 단계에서는 SSO URL 및 대상 URI와 같은 SAML 설정을 제공합니다.

Screen-Shot-2021-02-19-at-4.04.10-PM

마지막으로 통합에 대한 피드백을 제공할 수 있습니다.

Screen-Shot-2021-02-19-at-4.04.29-PM

3.3. 설정 지침 보기

완료되면 Spring Boot 앱에 대한 설정 지침을 볼 수 있습니다.

Screen-Shot-2021-02-19-at-4.04.56-PM

참고: Spring Security 구성에서 추가로 필요한 IdP 발급자 URL 및 IdP 메타데이터 XML과 같은 지침을 복사해야 합니다.

Screen-Shot-2021-02-19-at-4.06.33-PM

4. 스프링 부트 설정

spring-boot-starter-webspring-boot-starter-security 같은 일반적인 Maven 의존성 외에 spring-security-saml2-core 의존성 이 필요합니다 .

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

또한 Spring-security-saml2-core 의존성 에 필요한 최신 opensaml jar 를 다운로드하려면 Shibboleth 리포지토리 를 추가 해야 합니다.

<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: "<code class="language-xml">2.7.2" 컴파일 그룹: 'org.springframework.boot', 이름: 'spring-boot-starter-security', 버전: " 2.7.2" 컴파일 그룹: 'org.springframework.security.extensions', 이름: 'spring-security-saml2- 코어', 버전: "1.0.10.RELEASE"

5. 스프링 시큐리티 설정

이제 Okta SAML 설정 및 Spring Boot 프로젝트가 준비되었으므로 SAML 2.0과 Okta의 통합에 필요한 Spring Security 구성부터 시작하겠습니다.

5.1. SAML 진입점

먼저 SAML 인증을 위한 진입점으로 작동할 SAMLEntryPoint 클래스의 빈을 생성합니다.

@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 빈을 사용하면 사용자 인증을 요청하는 SP에서 IdP로 전송되는 요청의 매개변수를 설정할 수 있습니다.

5.2. 로그인 및 로그아웃

다음으로 / discovery, / login 및 / logout 과 같은 SAML URI에 대한 몇 가지 필터를 생성해 보겠습니다 .

@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. 메타데이터 처리

이제 IdP 메타데이터 XML을 SP에 제공합니다. 사용자가 로그인한 후 리디렉션해야 하는 SP Endpoints을 IdP에 알리는 데 도움이 됩니다.

따라서 Spring SAML이 메타데이터를 처리할 수 있도록 MetadataGenerator 빈을 구성합니다.

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;
}

SP와 IdP 간의 교환을 암호화 하려면 MetadataGenerator 빈에 KeyManager 인스턴스가 필요합니다 .

@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 에 Keystore를 생성하고 제공해야 합니다 . JRE 명령을 사용하여 자체 서명된 키와 키 저장소를 만들 수 있습니다.

keytool -genkeypair -alias baeldungspringsaml -keypass baeldungsamlokta -keystore saml-keystore.jks

5.4. MetadataManager

그런 다음 ExtendedMetadataDelegate 인스턴스 를 사용하여 Spring Boot 애플리케이션에 IdP 메타데이터를 구성합니다 .

@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;
}

여기에서는 설정 지침을 보는 동안 Okta 개발자 계정에서 복사한 IdP 메타데이터 XML이 포함된 sso.xml 파일에서 메타데이터를 구문 분석했습니다.

마찬가지로 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 구현

마지막으로 ExpiringUsernameAuthenticationToken 클래스의 인스턴스를 확인하고 획득한 권한을 설정하려면 SAMLAuthenticationProvider 클래스 의 사용자 지정 구현이 필요합니다 .

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 를 SecurityConfig 클래스 의 빈으로 구성해야 합니다.

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

5.8. Security 구성

마지막으로 이미 논의된 samlEntryPointsamlFilter 를 사용하여 기본 HTTP Security을 구성합니다 .

@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");
      });
}

짜잔! 사용자가 IdP에 로그인한 다음 IdP에서 XML 형식으로 사용자의 인증 세부 정보를 받을 수 있도록 하는 Spring Security SAML 구성을 완료했습니다. 마지막으로 웹 앱에 대한 액세스를 허용하기 위해 사용자 토큰을 인증합니다.

6. 홈컨트롤러

이제 Spring Security SAML 구성이 Okta 개발자 계정 설정과 함께 준비되었으므로 간단한 컨트롤러를 설정하여 랜딩 페이지와 홈페이지를 제공할 수 있습니다 .

6.1. 색인 및 인증 매핑

 먼저 기본 대상 URI (/) 및 / 인증 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 "/";
    }
}

그런 다음 사용자가 로그인 링크 를 사용하여 Okta SAML 인증을 리디렉션할 수 있도록 하는 간단한 index.html 을 추가합니다.

<!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/ 에서 액세스할 준비가 되었습니다 .

Screen-Shot-2021-02-24-at-7.20.44-AM
로그인 링크 를 클릭하면 Okta 로그인 페이지가 열립니다 .

Screen-Shot-2021-02-24-at-7.21.07-AM

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>

성공적으로 로그인하면 홈페이지가 표시됩니다.

Screen-Shot-2021-02-24-at-7.22.17-AM

7. 결론

이 예제에서는 Spring Security SAML과 Okta의 통합에 대해 논의했습니다.

먼저 SAML 2.0 웹 통합으로 Okta 개발자 계정을 설정합니다. 그런 다음 필요한 Maven 의존성을 사용하여 Spring Boot 프로젝트를 만들었습니다.

다음으로 samlEntryPoint , samlFilter , 메타데이터 처리 및 SAML 프로세서와 같은 Spring Security SAML에 필요한 모든 설정을 수행했습니다 .

마지막 으로 Okta와의 SAML 통합을 테스트하기 위해 컨트롤러와 색인 과 같은 몇 개의 페이지를 만들었습니다 .

평소와 같이 소스 코드는  GitHub 에서 사용할 수 있습니다 .

Security footer banner