이 문서는 새로운 Spring Security OAuth 2.0 스택으로 업데이트되었습니다. 그러나 레거시 스택을 사용하는 사용방법(예제) 는 계속 사용할 수 있습니다.

1. 개요

이 예제에서는 Spring Security로 OIDC(OpenID Connect)를 설정하는 데 중점을 둘 것입니다.

우리는 이 사양의 다양한 측면을 제시하고 Spring Security가 OAuth 2.0 클라이언트에서 이를 구현하기 위해 제공하는 지원을 볼 것입니다.

2. 빠른 OpenID 연결 소개

OpenID Connect 는 OAuth 2.0 프로토콜 위에 구축된 ID 계층입니다.

따라서 OIDC, 특히 인증 코드 흐름에 대해 알아보기 전에 OAuth 2.0 을 아는 것이 정말 중요합니다 .

OIDC 사양 제품군은 광범위합니다. 여기에는 여러 그룹으로 제공되는 핵심 기능과 기타 여러 선택적 기능이 포함됩니다. 주요 내용은 다음과 같습니다.

  • 핵심 – 최종 사용자 정보 전달을 위한 클레임 인증 및 사용
  • 검색 – 클라이언트가 OpenID 공급자에 대한 정보를 동적으로 결정할 수 있는 방법을 규정합니다.
  • 동적 등록 – 클라이언트가 공급자에 등록할 수 있는 방법 지시
  • 세션 관리 – OIDC 세션 관리 방법 정의

또한 이 문서에서는 이 사양을 지원하는 OAuth 2.0 인증 서버를 구분하여 OpenID 공급자(OP)와 OIDC를 신뢰 당사자(RP)로 사용하는 OAuth 2.0 클라이언트라고 합니다. 이 문서에서는 이 용어를 사용합니다.

클라이언트가 인증 요청에 openid  범위를 추가하여 이 확장의 사용을 요청할 수 있다는 점도 주목할 가치가 있습니다 .

마지막으로 이 사용방법(예제)에서는 OP가 최종 사용자 정보를 ID 토큰이라는 JWT로 내보내는 것을 아는 것이 유용합니다.

이제 OIDC 세계로 더 깊이 들어갈 준비가 되었습니다.

3. 프로젝트 설정

실제 개발에 집중하기 전에 OAuth 2.0 클라이언트를 OpenID 공급자에 등록해야 합니다.

이 경우 Google을 OpenID 공급자로 사용합니다. 이 지침 에 따라 플랫폼에 클라이언트 애플리케이션을 등록할 수 있습니다. openid 범위는 기본적으로 존재합니다

이 프로세스에서 설정한 리디렉션 URI는 서비스( http://localhost:8081/login/oauth2/code/google )의 엔드포인트입니다 .

이 과정에서 Client ID와 Client Secret을 얻어야 합니다.

3.1. 메이븐 구성

프로젝트 pom 파일에 이러한 의존성을 추가하여 시작하겠습니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

스타터 아티팩트는 다음을 포함하여 모든 Spring Security Client 관련 의존성을 집계합니다.

  • OAuth 2.0 로그인 및 클라이언트 기능에 대한 spring-security-oauth2-client 의존성
  • JWT 지원을 위한 JOSE 라이브러리

평소와 같이 Maven Central 검색 엔진 을 사용하여 이 아티팩트의 최신 버전을 찾을 수 있습니다 .

4. Spring Boot를 이용한 기본 설정

먼저 Google에서 방금 생성한 클라이언트 등록을 사용하도록 애플리케이션을 구성하는 것으로 시작합니다.

Spring Boot를 사용하면 두 가지 애플리케이션 속성을 정의하기만 하면 되므로 이를 매우 쉽게 수행할 수 있습니다 .

spring:
  security:
    oauth2:
      client:
        registration: 
          google: 
            client-id: <client-id>
            client-secret: <secret>

이제 응용 프로그램을 시작하고 Endpoints에 액세스해 보겠습니다. OAuth 2.0 클라이언트의 Google 로그인 페이지로 리디렉션되는 것을 볼 수 있습니다.

정말 간단해 보이지만 내부에는 많은 일이 있습니다. 다음으로 Spring Security가 이를 어떻게 해결하는지 살펴보겠습니다.

이전에 WebClient 및 OAuth 2 지원 게시물 에서 Spring Security가 OAuth 2.0 인증 서버 및 클라이언트를 처리하는 방법에 대한 내부를 분석했습니다.

여기에서 우리는 ClientRegistration 인스턴스를 성공적으로 구성하기 위해 클라이언트 ID와 클라이언트 암호 외에 추가 데이터를 제공해야 한다는 것을 확인 했습니다.

그래서 이것은 어떻게 작동합니까?

Google은 잘 알려진 공급자이므로 프레임워크는 일을 더 쉽게 하기 위해 몇 가지 미리 정의된 속성을 제공합니다.

CommonOAuth2Provider 열거형 에서 이러한 구성을 살펴볼 수 있습니다 .

Google의 경우 열거 유형은 다음과 같은 속성을 정의합니다.

  • 사용될 기본 범위
  • 권한 부여 Endpoints
  • 토큰 Endpoints
  • OIDC Core 사양의 일부이기도 한 UserInfo 엔드포인트

4.1. 사용자 정보 액세스

Spring Security는 OidcUser  엔터티 인 OIDC 공급자에 등록된 사용자 Principal의 유용한 표현을 제공합니다  .

기본  OAuth2AuthenticatedPrincipal 메서드 외에도 이 엔터티는 몇 가지 유용한 기능을 제공합니다.

  • ID 토큰 값과 여기에 포함된 클레임 검색
  • UserInfo Endpoints에서 제공하는 클레임 ​​가져오기
  • 두 세트의 집계 생성

컨트롤러에서 이 엔터티에 쉽게 액세스할 수 있습니다.

@GetMapping("/oidc-principal")
public OidcUser getOidcUserPrincipal(
  @AuthenticationPrincipal OidcUser principal) {
    return principal;
}

또는  Bean 에서 SecurityContextHolder 를 사용할 수 있습니다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
    OidcUser principal = ((OidcUser) authentication.getPrincipal());
    
    // ...
}

Security 주체를 검사하면 여기에서 사용자 이름, 이메일, 프로필 사진 및 로케일과 같은 많은 유용한 정보를 볼 수 있습니다.

게다가 Spring은 " SCOPE_ " 라는 접두어가 붙은 제공자로부터 받은 범위를 기반으로 프린시펄에 권한을 추가한다는 점에 유의하는 것이 중요합니다 . 예를 들어 openid 범위는  권한이 부여 된 SCOPE_openid  가 됩니다.

이러한 권한은 특정 리소스에 대한 액세스를 제한하는 데 사용할 수 있습니다.

@EnableWebSecurity
public class MappedAuthorities {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests
            .mvcMatchers("/my-endpoint")
              .hasAuthority("SCOPE_openid")
            .anyRequest().authenticated()
          );
        return http.build();
    }
}

5. OIDC 실행

지금까지 Spring Security를 ​​사용하여 OIDC 로그인 솔루션을 쉽게 구현하는 방법에 대해 알아보았습니다.

우리는 사용자 식별 프로세스를 확장 가능한 방식으로 상세하고 유용한 정보를 제공하는 OpenID 제공자에게 위임함으로써 그것이 가져다주는 이점을 보았습니다.

하지만 사실 지금까지는 OIDC 관련 측면을 다룰 필요가 없었습니다. 이는 Spring이 우리를 위해 대부분의 작업을 수행하고 있음을 의미합니다.

따라서 이 사양이 어떻게 실행되고 이를 최대한 활용할 수 있는지 더 잘 이해하기 위해 뒤에서 무슨 일이 일어나고 있는지 살펴보겠습니다.

5.1. 로그인 프로세스

이를 명확하게 확인하기 위해 RestTemplate  로그를 활성화하여 서비스가 수행 중인 요청을 확인합니다.

logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG

지금 Security 엔드포인트를 호출하면 서비스가 일반 OAuth 2.0 인증 코드 흐름을 수행하는 것을 볼 수 있습니다. 우리가 말했듯이 이 사양은 OAuth 2.0 위에 구축되었기 때문입니다.

몇 가지 차이점이 있습니다.

먼저 사용 중인 공급자와 구성한 범위에 따라 서비스가 처음에 언급한 UserInfo Endpoints을 호출하는 것을 볼 수 있습니다.

즉, 권한 부여 응답이 프로필 , 이메일 , 주소 또는 전화  범위 중 하나 이상을 검색하는 경우 프레임워크는 추가 정보를 얻기 위해 UserInfo 엔드포인트를 호출합니다.

모든 것이 Google이 프로필이메일  범위를 검색해야 함을 나타내지만 — 인증 요청에서 사용하고 있기 때문에 — OP는 대신 사용자 정의 상대를 검색합니다. https://www.googleapis.com/auth/userinfo.email 및  https://www.googleapis.com/auth/userinfo.profile 이므로 Spring은 Endpoints을 호출하지 않습니다.

이것은 우리가 얻고 있는 모든 정보가 ID 토큰의 일부임을 의미합니다.

자체 OidcUserService 인스턴스 를 생성하고 제공하여 이 동작에 적응할 수 있습니다 .

@Configuration
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");

        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);

        http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest()
            .authenticated())
            .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint()
                .oidcUserService(googleUserService));
        return http.build();
    }
}

두 번째 차이점은 JWK Set URI에 대한 호출입니다. JWS 및 JWK 게시물 에서 설명한 것처럼 JWT 형식의 ID 토큰 서명을 확인하는 데 사용됩니다.

다음으로 ID Token을 자세히 분석하겠습니다.

5.2. ID 토큰

당연히 OIDC 사양은 다양한 시나리오를 다루고 적용합니다. 이 경우 인증 코드 흐름을 사용하고 있으며 프로토콜은 액세스 토큰과 ID 토큰이 모두 토큰 Endpoints 응답의 일부로 검색됨을 나타냅니다.

이전에 말했듯이 OidcUser 엔터티에는 ID 토큰에 포함된 클레임과 jwt.io 를 사용하여 검사할 수 있는 실제 JWT 형식 토큰이 포함되어 있습니다 .

또한 Spring은 명세에 의해 정의된 표준 클레임을 깨끗한 방식으로 얻기 위해 많은 편리한 getter를 제공합니다.

ID 토큰에 몇 가지 필수 클레임이 포함되어 있음을 볼 수 있습니다.

  • URL 형식의 발급자 식별자(예: ' https://accounts.google.com ')
  • 발급자가 포함하는 최종 사용자의 참조인 주체 ID
  • 토큰 만료 시간
  • 토큰이 발행된 시간
  • 우리가 구성한 OAuth 2.0 클라이언트 ID를 포함할 대상

또한 이전에 언급한 것과 같은 많은 OIDC 표준 클레임 을 포함합니다( name , locale , picture , email ).

이것이 표준이므로 많은 공급자가 이러한 필드 중 적어도 일부를 검색하여 더 간단한 솔루션 개발을 용이하게 할 것으로 기대할 수 있습니다.

5.3. 주장 및 범위

상상할 수 있듯이 OP가 검색한 클레임은 우리(또는 Spring Security)가 구성한 범위와 일치합니다.

OIDC는 OIDC에서 정의한 클레임을 요청하는 데 사용할 수 있는 몇 가지 범위를 정의합니다.

  • profile 기본 프로필 요청에 사용할 수 있는 클레임(예: name , preferred_usernamepicture 등)
  • email , emailemail_verified 클레임 에 액세스
  • 주소
  • phone , phone_numberphone_number_verified 클레임 요청

Spring이 아직 지원하지 않더라도 사양은 Authorization Request에서 단일 클레임을 지정하여 요청하는 것을 허용합니다.

6. OIDC 검색을 위한 Spring 지원

소개에서 설명한 것처럼 OIDC에는 핵심 목적 외에도 다양한 기능이 포함되어 있습니다.

이 섹션과 다음에서 분석할 기능은 OIDC에서 선택 사항입니다. 따라서 이를 지원하지 않는 OP가 있을 수 있음을 이해하는 것이 중요합니다.

이 사양은 RP가 OP를 검색하고 OP와 상호 작용하는 데 필요한 정보를 얻기 위한 검색 메커니즘을 정의합니다.

간단히 말해서 OP는 표준 메타데이터의 JSON 문서를 제공합니다. 이 정보는 발급자 위치의 잘 알려진 Endpoints인 /.well-known/openid-configuration 에서 제공해야 합니다 .

Spring은  하나의 간단한 속성인 발급자 위치 로 ClientRegistration 을 구성할 수 있도록 함으로써 이점을 얻습니다.

그러나 이것을 명확하게 보기 위해 예를 들어 보겠습니다.

사용자 지정 ClientRegistration 인스턴스를 정의 합니다.

spring:
  security:
    oauth2:
      client:
        registration: 
          custom-google: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          custom-google:
            issuer-uri: https://accounts.google.com

이제 애플리케이션을 다시 시작하고 로그를 확인하여 애플리케이션이 시작 프로세스에서 openid-configuration  엔드포인트를 호출하는지 확인할 수 있습니다.

Google에서 제공하는 정보를 보기 위해 이 Endpoints을 탐색할 수도 있습니다.

https://accounts.google.com/.well-known/openid-configuration

예를 들어 서비스가 사용해야 하는 Authorization, Token 및 UserInfo Endpoints과 지원되는 범위를 볼 수 있습니다.

서비스가 시작될 때 Discovery 엔드포인트를 사용할 수 없는 경우 앱이 시작 프로세스를 성공적으로 완료할 수 없다는 점에 특히 유의해야 합니다.

7. OpenID Connect 세션 관리

이 사양은 다음을 정의하여 핵심 기능을 보완합니다.

  • RP가 OpenID 공급자에서 로그아웃한 최종 사용자를 로그아웃할 수 있도록 OP에서 최종 사용자의 로그인 상태를 지속적으로 모니터링하는 다양한 방법
  • 최종 사용자가 OP에서 로그아웃할 때 알림을 받기 위해 클라이언트 등록의 일부로 OP에 RP 로그아웃 URI를 등록할 수 있는 가능성
  • 최종 사용자가 사이트에서 로그아웃했으며 OP에서도 로그아웃하기를 원할 수 있음을 OP에 알리는 메커니즘

당연히 모든 OP가 이러한 항목을 모두 지원하는 것은 아니며 이러한 솔루션 중 일부는 User-Agent를 통해 프런트 엔드 구현에서만 구현할 수 있습니다.

이 예제에서는 List의 마지막 항목인 RP 시작 로그아웃에 대해 Spring이 제공하는 기능에 초점을 맞출 것입니다.

이 시점에서 애플리케이션에 로그인하면 일반적으로 모든 엔드포인트에 액세스할 수 있습니다.

로그아웃( /logout  엔드포인트 호출 )하고 나중에 Security 리소스에 요청하면 다시 로그인하지 않고도 응답을 받을 수 있음을 알 수 있습니다.

그러나 이것은 사실이 아닙니다. 브라우저 디버그 콘솔에서 네트워크 탭을 검사하면 두 번째로 Security 엔드포인트에 도달할 때 OP 인증 엔드포인트로 리디렉션되는 것을 볼 수 있습니다. 그리고 우리는 여전히 거기에 로그인되어 있기 때문에 흐름이 투명하게 완료되어 거의 즉시 Security 엔드포인트로 끝납니다.

물론 이것은 경우에 따라 원하는 동작이 아닐 수도 있습니다. 이를 처리하기 위해 이 OIDC 메커니즘을 구현하는 방법을 살펴보겠습니다.

7.1. OpenID 공급자 구성

이 경우 Okta 인스턴스를 OpenID 공급자로 구성하고 사용합니다. 인스턴스를 생성하는 방법에 대해 자세히 다루지는 않겠지만 Spring Security의 기본 콜백 엔드포인트가 /login/oauth2/code/okta 라는 점을 염두에 두고 이 사용방법(예제) 의 단계를 따를 수 있습니다 .

애플리케이션에서 속성을 사용하여 클라이언트 등록 데이터를 정의할 수 있습니다.

spring:
  security:
    oauth2:
      client:
        registration: 
          okta: 
            client-id: <client-id>
            client-secret: <secret>
        provider:
          okta:
            issuer-uri: https://dev-123.okta.com

OIDC는 검색 문서에서 end_session_endpoint  요소 로 OP 로그아웃 엔드포인트를 지정할 수 있음을 나타냅니다  .

7.2. LogoutSuccessHandler 구성 _

다음 으로 사용자 지정 LogoutSuccessHandler 인스턴스 를 제공하여 HttpSecurity  로그아웃 논리 를 구성해야 합니다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .authorizeRequests(authorizeRequests -> authorizeRequests
        .mvcMatchers("/home").permitAll()
        .anyRequest().authenticated())
      .oauth2Login(oauthLogin -> oauthLogin.permitAll())
      .logout(logout -> logout
        .logoutSuccessHandler(oidcLogoutSuccessHandler()));
    return http.build();
}

이제 Spring Security에서 제공하는 특수 클래스인 OidcClientInitiatedLogoutSuccessHandler를 사용하여 이 목적을 위해 LogoutSuccessHandler 를 생성하는 방법을 살펴 보겠습니다 .

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
      new OidcClientInitiatedLogoutSuccessHandler(
        this.clientRegistrationRepository);

    oidcLogoutSuccessHandler.setPostLogoutRedirectUri(
      URI.create("http://localhost:8081/home"));

    return oidcLogoutSuccessHandler;
}

따라서 OP 클라이언트 구성 패널에서 이 URI를 유효한 로그아웃 리디렉션 URI로 설정해야 합니다.

처리기를 구성하는 데 사용하는 모든 것이 컨텍스트에 있는 ClientRegistrationRepository  빈 이기 때문에 분명히 OP 로그아웃 구성은 클라이언트 등록 설정에 포함되어 있습니다 .

이제 어떻게 될까요?

애플리케이션에 로그인한 후 Spring Security에서 제공 하는 /logout  엔드포인트 에 요청을 보낼 수 있습니다 .

브라우저 디버그 콘솔에서 네트워크 로그를 확인하면 구성한 리디렉션 URI에 최종적으로 액세스하기 전에 OP 로그아웃 Endpoints으로 리디렉션되는 것을 볼 수 있습니다.

다음에 인증이 필요한 애플리케이션의 엔드포인트에 액세스할 때 권한을 얻기 위해 OP 플랫폼에 다시 로그인해야 합니다.

8. 결론

요약하자면, 이 기사에서는 OpenID Connect에서 제공하는 솔루션과 Spring Security를 ​​사용하여 그 중 일부를 구현하는 방법에 대해 많은 것을 배웠습니다.

항상 그렇듯이 모든 전체 예제는 GitHub 에서 찾을 수 있습니다 .

Security footer banner