๐Ÿงญ Spring Authorization Server๋กœ ์ž์ฒด ์ธ์ฆ ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ

okorionยท2025๋…„ 10์›” 6์ผ

๐Ÿ” Spring Security 6

๋ชฉ๋ก ๋ณด๊ธฐ
9/10

Keycloak ์—†์ด, Spring Security ์ƒํƒœ๊ณ„ ์•ˆ์—์„œ ์™„์ „ํ•œ OAuth2 & OpenID Connect ์ธ์ฆ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ“˜ ๋“ค์–ด๊ฐ€๋ฉฐ โ€” โ€œ์ž„๋Œ€ vs ์ž๊ฐ€ ๊ตฌ์ถ•โ€์˜ ๋น„์œ 

์ด์ „ ํŽธ์—์„œ๋Š” Keycloak์„ ์ด์šฉํ•ด ์™ธ๋ถ€ ์ธ์ฆ ์„œ๋ฒ„๋ฅผ โ€œ์ž„๋Œ€โ€ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ดํŽด๋ดค๋‹ค.

์ด๋ฒˆ ํŽธ์—์„œ๋Š” ๊ฐ™์€ ๊ธฐ๋Šฅ์„ โ€œ์ง์ ‘ ๊ตฌ์ถ•โ€ํ•œ๋‹ค.

๊ด€์ Keycloak (์ž„๋Œ€ํ˜•)Spring Authorization Server (์ž๊ฐ€ ๊ตฌ์ถ•ํ˜•)
๊ด€๋ฆฌ ์ฃผ์ฒด์™ธ๋ถ€ ์†”๋ฃจ์…˜์šฐ๋ฆฌ ์„œ๋น„์Šค ๋‚ด๋ถ€
๊ตฌ์„ฑ ์†๋„๋น ๋ฆ„ (Ready-made)๋А๋ฆผ (์ง์ ‘ ์„ค์ •)
์œ ์—ฐ์„ฑ / ํ™•์žฅ์„ฑ์ œํ•œ์ ๋งค์šฐ ๋†’์Œ
์šด์˜ ๋ณต์žก๋„๋‚ฎ์Œ๋†’์Œ
๋ณด์•ˆ ์ œ์–ด๊ถŒ๋ถ€๋ถ„ ์œ„์ž„์™„์ „ ์†Œ์œ 

์ฆ‰, Keycloak์€ โ€œ๋น ๋ฅธ MVP์šฉโ€, Spring Authorization Server(SAS)๋Š” โ€œ๊ธฐ์—… ๋‚ด๋ถ€ ํ‘œ์ค€ํ™”์šฉโ€์ด๋‹ค.


#1. Spring Authorization Server๋ž€?

Spring Authorization Server๋Š”

OAuth2 ๋ฐ OpenID Connect 1.0 ํ”„๋กœํ† ์ฝœ์„ ์ง€์›ํ•˜๋Š” ๊ณต์‹ Spring ํ”„๋กœ์ ํŠธ๋‹ค.

(๋ณ„๋„ ์Šคํƒ€ํ„ฐ: org.springframework.security:spring-security-oauth2-authorization-server)

โœ… Keycloak์ด๋‚˜ Auth0 ์—†์ด๋„,

Access Token ยท Refresh Token ยท ID Token ยท JWK ์„œ๋ช… ํ‚ค ๊ด€๋ฆฌ๊นŒ์ง€ ๋ชจ๋‘ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๋‹ค.


#2. ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”


#3. ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์„ธํŒ…

pom.xml

<dependencies>
  <!-- Spring Boot Web / Security -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <!-- Spring Authorization Server -->
  <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
  </dependency>

  <!-- JWT / JWK -->
  <dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>oauth2-oidc-sdk</artifactId>
  </dependency>
</dependencies>

#4. SecurityFilterChain ๊ตฌ์„ฑ

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
      .formLogin(Customizer.withDefaults());
    return http.build();
  }
}

์ด ๊ธฐ๋ณธ ์ฒด์ธ์€ Authorization Server ์™ธ์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

๋‹ค์Œ ๋‹จ๊ณ„์—์„œ Authorization Server์šฉ ์ฒด์ธ์„ ๋ณ„๋„๋กœ ์ถ”๊ฐ€ํ•œ๋‹ค.


#5. Authorization Server ์„ค์ •

@Configuration
public class AuthorizationServerConfig {

  @Bean
  SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
        .oidc(Customizer.withDefaults()); // OpenID Connect ํ™œ์„ฑํ™”
    return http.formLogin(Customizer.withDefaults()).build();
  }

  @Bean
  RegisteredClientRepository registeredClientRepository() {
    RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
      .clientId("eazybank-client")
      .clientSecret("{noop}secret")
      .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
      .redirectUri("http://localhost:4200/login/oauth2/code/")
      .scope(OidcScopes.OPENID)
      .scope("read")
      .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(5)).build())
      .build();

    return new InMemoryRegisteredClientRepository(client);
  }

  @Bean
  JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = JwksKeys.generateRsa(); // ์œ ํ‹ธ๋ฆฌํ‹ฐ๋กœ RSA ํ‚ค ์ƒ์„ฑ
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (selector, context) -> selector.select(jwkSet);
  }

  @Bean
  ProviderSettings providerSettings() {
    return ProviderSettings.builder()
      .issuer("http://localhost:9000")
      .build();
  }
}

#6. RSA ํ‚ค ์ƒ์„ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ

public final class JwksKeys {
  public static RSAKey generateRsa() {
    KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
      .privateKey(privateKey)
      .keyID(UUID.randomUUID().toString())
      .build();
  }
}

๐Ÿ” JWK (JSON Web Key)

JWT ์„œ๋ช…์— ์‚ฌ์šฉ๋˜๋Š” ๊ณต๊ฐœํ‚ค ์ •๋ณด๋ฅผ JSON์œผ๋กœ ํ‘œํ˜„ํ•œ ๊ฐ์ฒด.

Resource Server๋Š” ์ด ๊ณต๊ฐœํ‚ค๋กœ JWT์˜ ์„œ๋ช…์„ ๊ฒ€์ฆํ•œ๋‹ค.


#7. Resource Server ์—ฐ๋™

application.yml

server:
  port: 9001

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:9000/oauth2/jwks

SecurityConfig.java

@Bean
SecurityFilterChain resourceServerSecurity(HttpSecurity http) throws Exception {
  http
    .authorizeHttpRequests(auth -> auth
      .requestMatchers("/public/**").permitAll()
      .anyRequest().authenticated())
    .oauth2ResourceServer(oauth2 -> oauth2.jwt());
  return http.build();
}

#8. Authorization Code + PKCE Flow ๊ฒ€์ฆ

Authorization Request ์˜ˆ์‹œ

GET http://localhost:9000/oauth2/authorize?
  response_type=code&
  client_id=eazybank-client&
  scope=openid%20read&
  redirect_uri=http://localhost:4200/login/oauth2/code/&
  code_challenge_method=S256&
  code_challenge=AbCdEf123...

Access Token ์š”์ฒญ

POST http://localhost:9000/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:secret)

grant_type=authorization_code
&code=<AUTH_CODE>
&redirect_uri=http://localhost:4200/login/oauth2/code/
&code_verifier=AbCdEf123...

์‘๋‹ต

{
  "access_token": "eyJraWQiOiJ...",
  "refresh_token": "eyJhbGciOiJ...",
  "id_token": "eyJhbGciOiJSUzI1...",
  "token_type": "Bearer",
  "expires_in": 300
}

#9. ID Token๊ณผ OpenID Connect ํ™•์žฅ

Authorization Server ๊ตฌ์„ฑ์—์„œ

oidc(Customizer.withDefaults()) ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ฉด

ID Token ๋ฐœ๊ธ‰์ด ์ž๋™ ์ง€์›๋œ๋‹ค.

ID Token ์˜ˆ์‹œ ํŽ˜์ด๋กœ๋“œ

{
  "sub": "user1",
  "iss": "http://localhost:9000",
  "aud": "eazybank-client",
  "iat": 1737450000,
  "exp": 1737450300,
  "email": "user1@eazybank.com"
}

Resource Server๋Š” access_token๋งŒ ๊ฒ€์ฆํ•˜์ง€๋งŒ,

ํด๋ผ์ด์–ธํŠธ(UI)๋Š” id_token์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•œ๋‹ค.


#10. Token Customizer๋กœ Claim ํ™•์žฅ

@Bean
OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
  return context -> {
    Authentication principal = context.getPrincipal();
    if (context.getTokenType().getValue().equals("access_token")) {
      context.getClaims().claim("user_email", principal.getName());
      context.getClaims().claim("authorities",
        principal.getAuthorities().stream()
          .map(GrantedAuthority::getAuthority)
          .toList());
    }
  };
}

โžก๏ธ JWT ๋‚ด๋ถ€์— ์‚ฌ์šฉ์ž ์ •๋ณด, ์—ญํ• , ์กฐ์ง, ๋ถ€์„œ ๋“ฑ ์ถ”๊ฐ€ Claim ์‚ฝ์ž… ๊ฐ€๋Šฅ.


#11. Spring Authorization Server vs Keycloak ๋น„๊ต

ํ•ญ๋ชฉKeycloakSpring Authorization Server
๋ฐฐํฌ ํ˜•ํƒœ์™„์ œํ’ˆ ์„œ๋ฒ„ (Docker, UI ์ œ๊ณต)ํ”„๋ ˆ์ž„์›Œํฌ (์ง์ ‘ ์ฝ”๋“œ ์ž‘์„ฑ)
์ธ์ฆ ๋ฐฉ์‹OIDC, SAML, LDAP ๋“ฑ ์™„๋น„OAuth2, OIDC (ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์ค‘์‹ฌ)
ํ™•์žฅ์„ฑํ”Œ๋Ÿฌ๊ทธ์ธ ๊ธฐ๋ฐ˜ (SPI)์ฝ”๋“œ ๊ธฐ๋ฐ˜ (Spring Bean)
๊ด€๋ฆฌ UI์žˆ์Œ์—†์Œ (์ง์ ‘ ๊ตฌํ˜„)
๋ณด์•ˆ ์ œ์–ด๊ถŒ์ œํ•œ์  (์„ค์ • ๊ธฐ๋ฐ˜)์™„์ „ ์ œ์–ด ๊ฐ€๋Šฅ
๋Ÿฌ๋‹์ปค๋ธŒ๋‚ฎ์Œ์ค‘๊ฐ„~๋†’์Œ
์ถ”์ฒœ ์‚ฌ์šฉ์ฒ˜๋‹ค์ˆ˜ ์„œ๋น„์Šค ํ†ตํ•ฉ, ๊ธฐ์—… SSO๋‚ด๋ถ€ ์‹œ์Šคํ…œ, ๊ณ ์œ  ์ธ์ฆ ์š”๊ตฌ ์„œ๋น„์Šค

#12. ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ†ตํ•ฉ ์•„ํ‚คํ…์ฒ˜ ์˜ˆ์‹œ

  • Authorization Server: OAuth2 + OIDC ๋ฐœ๊ธ‰ ๋‹ด๋‹น
  • Resource Server: JWT ๊ฒ€์ฆ ๋ฐ ๊ถŒํ•œ ์ธ๊ฐ€
  • Client: PKCE ๊ธฐ๋ฐ˜ ์ธ์ฆ ์š”์ฒญ

#13. ์šด์˜ยท๋ณด์•ˆ ํŒ

๊ตฌ๋ถ„๊ถŒ์žฅ ์„ค์ •
Access Token TTL5๋ถ„ ์ดํ•˜
Refresh Token TTL30๋ถ„ ์ดํ•˜
HTTPS ํ•„์ˆ˜Local ์™ธ ํ™˜๊ฒฝ
JWK ๊ด€๋ฆฌ์™ธ๋ถ€ Vault๋‚˜ Cloud KMS ํ†ตํ•ฉ
๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง/oauth2/token, /oauth2/introspect ์š”์ฒญ ์ถ”์ 
DB ์—ฐ๋™RegisteredClient, Authorization, Consent ์—”ํ‹ฐํ‹ฐ๋ฅผ RDB๋กœ ์ด๊ด€ ๊ฐ€๋Šฅ (JdbcRegisteredClientRepository)

#14. Keycloak๊ณผ์˜ ์—ฐ๊ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค (Hybrid)

๊ธฐ์—… ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹ค์Œ ๊ตฌ์กฐ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

  • ์™ธ๋ถ€ ์„œ๋น„์Šค๋Š” Keycloak์œผ๋กœ SSO
  • ๋‚ด๋ถ€ ์ „์šฉ API๋Š” Spring Authorization Server ์‚ฌ์šฉ
  • ๋‘ ์„œ๋ฒ„ ๊ฐ„ Federation์œผ๋กœ ์‚ฌ์šฉ์ž ๋™๊ธฐํ™”

#15. ๊ฒฐ๋ก  โ€” โ€œ์™„๋ฒฝํ•œ ์ œ์–ด๊ถŒโ€์˜ ์‹œ์ž‘

Spring Authorization Server๋Š”

Keycloak๋ณด๋‹ค ๊ตฌ์ถ•์€ ์–ด๋ ต์ง€๋งŒ, ๋ณด์•ˆ ์ œ์–ด๊ถŒ๊ณผ ํ™•์žฅ์„ฑ์ด ์••๋„์ ์ด๋‹ค.

โœ… ์™„์ „ํ•œ ๋‚ด๋ถ€ ํ†ต์ œ (๋ณด์•ˆ์ •์ฑ…, Token ๊ตฌ์กฐ, Claim ์„ค๊ณ„)

โœ… Spring Security์™€ 100% ํ˜ธํ™˜

โœ… Code-level ํ™•์žฅ (OAuth2TokenCustomizer, ConsentService ๋“ฑ)

โœ… JWK / JWT / OIDC ์™„์ „ ๊ตฌํ˜„


#16. ์ฐธ๊ณ  ์•„ํ‚คํ…์ฒ˜ ๋น„๊ต ์š”์•ฝ

๊ตฌ๋ถ„KeycloakSpring Authorization Server
๋„์ž… ๋‚œ์ด๋„๋‚ฎ์Œ์ค‘๊ฐ„~๋†’์Œ
ํ™•์žฅ์„ฑ์ œํ•œ์  (์„ค์ • ์œ„์ฃผ)์ฝ”๋“œ ํ™•์žฅ ๋ฌดํ•œ
์œ ์ง€๋ณด์ˆ˜์—…๋ฐ์ดํŠธ ์˜์กด์ง์ ‘ ๋ฒ„์ „ ๊ด€๋ฆฌ
ํด๋ผ์šฐ๋“œ ์—ฐ๋™Kubernetes ์ตœ์ ํ™”Spring Cloud ์—ฐ๊ณ„ ์šฉ์ด
์ปค์Šคํ„ฐ๋งˆ์ด์ง•Admin ConsoleBean / Configurable Components

#17. ์ •๋ฆฌ

  • Keycloak์€ ์™ธ๋ถ€ ์†”๋ฃจ์…˜ ํ†ตํ•ฉํ˜•
  • Spring Authorization Server๋Š” ๋‚ด๋ถ€ ์ธ์ฆ ์ธํ”„๋ผ
  • ๋‘ ์ ‘๊ทผ์€ ๋ฐฐํƒ€์ ์ด์ง€ ์•Š์œผ๋ฉฐ ๊ณต์กด ๊ฐ€๋Šฅ
profile
Tech Blog

0๊ฐœ์˜ ๋Œ“๊ธ€