프로그래밍 PROGRAMMING/자바 JAVA AND FRAMEWORKS

Spring Authorization Server로 OAuth2 인증 서버 구축하기

매운할라피뇨 2026. 3. 20. 08:30
반응형
Spring Authorization Server로 OAuth2 인증 서버 구축


Spring Authorization Server는 Spring 생태계에서 공식적으로 지원하는 OAuth2 인증 서버 구현체로, 기존의 Spring Security OAuth2가 유지보수 종료(EOL)된 이후 사실상의 표준으로 자리 잡았습니다. 이 글에서는 Spring Authorization Server를 사용해 OAuth2 + OIDC(OpenID Connect) 인증 서버를 직접 구축하고, 클라이언트 등록부터 토큰 발급까지 전 과정을 실제 프로젝트 기준으로 설명합니다.


프로젝트 의존성 및 기본 설정

Spring Authorization Server는 Spring Boot 3.x 기준으로 spring-boot-starter-oauth2-authorization-server 의존성 하나로 시작할 수 있습니다. 내부적으로 Spring Security와 통합되어 있으므로 별도의 Security 스타터를 추가하지 않아도 됩니다.

아래는 build.gradle 설정 예시입니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 인증 서버 UI(로그인 페이지) 커스터마이징 시 thymeleaf 추가 가능
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

의존성을 추가하면 Spring Boot 자동 설정이 기본적인 AuthorizationServerSettings, JWKSource 등 빈을 구성합니다. 다만 운영 환경에서는 반드시 직접 빈을 오버라이드해야 합니다.


SecurityConfig와 AuthorizationServer 설정

Spring Authorization Server의 핵심은 두 개의 SecurityFilterChain 빈을 분리해 정의하는 구조입니다. 하나는 인증 서버 엔드포인트 전용, 다른 하나는 일반 사용자 인증용입니다.

다음은 최소 구성의 SecurityConfig 예시입니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 1. 인증 서버 전용 SecurityFilterChain
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults()); // OpenID Connect 1.0 활성화

        http
            .exceptionHandling(ex -> ex
                .defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"),
                    new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
            )
            .oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));

        return http.build();
    }

    // 2. 일반 사용자 인증용 SecurityFilterChain
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults()); // 기본 로그인 페이지 사용

        return http.build();
    }

    // 3. 인메모리 사용자 (개발/테스트용)
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("password")
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

@Order(1)로 지정된 필터 체인이 /oauth2/authorize, /oauth2/token 등 인증 서버 전용 경로를 먼저 처리합니다. applyDefaultSecurity()는 PKCE, 토큰 엔드포인트 등 OAuth2 스펙에 정의된 표준 설정을 자동으로 적용해 줍니다.


클라이언트 등록 및 JWK 설정

OAuth2 인증 서버가 동작하려면 RegisteredClientJWKSource 두 가지 빈이 반드시 필요합니다. RegisteredClient는 OAuth2 클라이언트 메타데이터(클라이언트 ID, 시크릿, 허용 Grant Type 등)를 정의하며, JWKSource는 JWT 서명에 사용할 RSA 키 쌍을 제공합니다.

@Configuration
public class AuthorizationServerConfig {

    // OAuth2 클라이언트 등록 (인메모리, 운영 시 DB 기반으로 교체)
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("my-client")
            .clientSecret("{noop}secret")                          // noop = 평문 (테스트용)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://localhost:8080/login/oauth2/code/my-client")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("read")
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(30))
                .refreshTokenTimeToLive(Duration.ofDays(7))
                .build())
            .build();

        return new InMemoryRegisteredClientRepository(client);
    }

    // RSA 키 쌍 생성 (JWT 서명용)
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);
        KeyPair keyPair = generator.generateKeyPair();

        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();

        return new ImmutableJWKSet<>(new JWKSet(rsaKey));
    }

    // JWT 디코더 (OIDC UserInfo 엔드포인트 등 내부 검증에 사용)
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    // 인증 서버 기본 설정 (issuer URL 포함)
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
            .issuer("http://localhost:9000") // 인증 서버의 공개 URL
            .build();
    }
}

실행 결과 확인: 서버 기동 후 http://localhost:9000/.well-known/openid-configuration에 접근하면 OIDC Discovery 메타데이터(토큰 엔드포인트, JWKS URI 등)가 JSON으로 반환됩니다.

운영 환경에서는 InMemoryRegisteredClientRepository 대신 JdbcRegisteredClientRepository를 사용해 클라이언트 정보를 DB에 영속하는 것이 권장됩니다. RSA 키 쌍 역시 매 재시작마다 재생성되지 않도록 Key Store(JKS 또는 PKCS12) 또는 AWS KMS 같은 외부 키 관리 서비스와 연동해야 합니다.


Authorization Code Flow 동작 확인

설정이 완료된 후 실제 Authorization Code Flow를 테스트하려면 아래 순서를 따릅니다.

  1. Authorization 요청: 브라우저에서 아래 URL 접근
  2. http://localhost:9000/oauth2/authorize ?response_type=code &client_id=my-client &redirect_uri=http://localhost:8080/login/oauth2/code/my-client &scope=openid profile read &state=random-state-value
  3. 로그인: 설정한 사용자(admin / password)로 로그인 및 동의 처리
  4. Token 교환: 리다이렉트된 code 파라미터를 사용해 토큰 요청
  5. curl -X POST http://localhost:9000/oauth2/token \ -u my-client:secret \ -d "grant_type=authorization_code" \ -d "code=<발급된_코드>" \ -d "redirect_uri=http://localhost:8080/login/oauth2/code/my-client"
  6. 응답 예시:
  7. { "access_token": "eyJra...", "refresh_token": "abc123...", "token_type": "Bearer", "expires_in": 1800, "id_token": "eyJra..." }

맺음말

Spring Authorization Server는 OAuth2 Authorization Code Flow, PKCE, Refresh Token, OIDC UserInfo 등 현대적인 인증 요구사항을 폭넓게 지원합니다. 특히 기존 Spring Security 기반 프로젝트와 자연스럽게 통합된다는 점에서 별도의 Keycloak이나 Auth0 같은 외부 서비스를 도입하기 부담스러운 팀에 적합한 선택지입니다.
다음 단계로는 JdbcRegisteredClientRepository를 통한 DB 기반 클라이언트 관리, 커스텀 토큰 클레임 추가(TokenCustomizer), Resource Server와의 연동을 다루어 볼 수 있습니다. 공식 문서와 샘플은 Spring Authorization Server 공식 사이트에서 확인할 수 있습니다.

반응형