Keycloak ์์ด, Spring Security ์ํ๊ณ ์์์ ์์ ํ OAuth2 & OpenID Connect ์ธ์ฆ ์๋ฒ๋ฅผ ๊ตฌํํ๊ธฐ
์ด์ ํธ์์๋ Keycloak์ ์ด์ฉํด ์ธ๋ถ ์ธ์ฆ ์๋ฒ๋ฅผ โ์๋โํ๋ ๋ฐฉ์์ ์ดํด๋ดค๋ค.
์ด๋ฒ ํธ์์๋ ๊ฐ์ ๊ธฐ๋ฅ์ โ์ง์ ๊ตฌ์ถโํ๋ค.
| ๊ด์ | Keycloak (์๋ํ) | Spring Authorization Server (์๊ฐ ๊ตฌ์ถํ) |
|---|---|---|
| ๊ด๋ฆฌ ์ฃผ์ฒด | ์ธ๋ถ ์๋ฃจ์ | ์ฐ๋ฆฌ ์๋น์ค ๋ด๋ถ |
| ๊ตฌ์ฑ ์๋ | ๋น ๋ฆ (Ready-made) | ๋๋ฆผ (์ง์ ์ค์ ) |
| ์ ์ฐ์ฑ / ํ์ฅ์ฑ | ์ ํ์ | ๋งค์ฐ ๋์ |
| ์ด์ ๋ณต์ก๋ | ๋ฎ์ | ๋์ |
| ๋ณด์ ์ ์ด๊ถ | ๋ถ๋ถ ์์ | ์์ ์์ |
์ฆ, Keycloak์ โ๋น ๋ฅธ MVP์ฉโ, Spring Authorization Server(SAS)๋ โ๊ธฐ์ ๋ด๋ถ ํ์คํ์ฉโ์ด๋ค.
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 ์๋ช ํค ๊ด๋ฆฌ๊น์ง ๋ชจ๋ ์ฒ๋ฆฌ ๊ฐ๋ฅํ๋ค.

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>
@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์ฉ ์ฒด์ธ์ ๋ณ๋๋ก ์ถ๊ฐํ๋ค.
@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();
}
}
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์ ์๋ช ์ ๊ฒ์ฆํ๋ค.
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();
}
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...
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
}
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์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํ์ํ๋ค.
@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 ์ฝ์ ๊ฐ๋ฅ.
| ํญ๋ชฉ | Keycloak | Spring Authorization Server |
|---|---|---|
| ๋ฐฐํฌ ํํ | ์์ ํ ์๋ฒ (Docker, UI ์ ๊ณต) | ํ๋ ์์ํฌ (์ง์ ์ฝ๋ ์์ฑ) |
| ์ธ์ฆ ๋ฐฉ์ | OIDC, SAML, LDAP ๋ฑ ์๋น | OAuth2, OIDC (ํต์ฌ ๊ธฐ๋ฅ ์ค์ฌ) |
| ํ์ฅ์ฑ | ํ๋ฌ๊ทธ์ธ ๊ธฐ๋ฐ (SPI) | ์ฝ๋ ๊ธฐ๋ฐ (Spring Bean) |
| ๊ด๋ฆฌ UI | ์์ | ์์ (์ง์ ๊ตฌํ) |
| ๋ณด์ ์ ์ด๊ถ | ์ ํ์ (์ค์ ๊ธฐ๋ฐ) | ์์ ์ ์ด ๊ฐ๋ฅ |
| ๋ฌ๋์ปค๋ธ | ๋ฎ์ | ์ค๊ฐ~๋์ |
| ์ถ์ฒ ์ฌ์ฉ์ฒ | ๋ค์ ์๋น์ค ํตํฉ, ๊ธฐ์ SSO | ๋ด๋ถ ์์คํ , ๊ณ ์ ์ธ์ฆ ์๊ตฌ ์๋น์ค |

| ๊ตฌ๋ถ | ๊ถ์ฅ ์ค์ |
|---|---|
| Access Token TTL | 5๋ถ ์ดํ |
| Refresh Token TTL | 30๋ถ ์ดํ |
| HTTPS ํ์ | Local ์ธ ํ๊ฒฝ |
| JWK ๊ด๋ฆฌ | ์ธ๋ถ Vault๋ Cloud KMS ํตํฉ |
| ๋ก๊ทธ ๋ชจ๋ํฐ๋ง | /oauth2/token, /oauth2/introspect ์์ฒญ ์ถ์ |
| DB ์ฐ๋ | RegisteredClient, Authorization, Consent ์ํฐํฐ๋ฅผ RDB๋ก ์ด๊ด ๊ฐ๋ฅ (JdbcRegisteredClientRepository) |
๊ธฐ์
ํ๊ฒฝ์์๋ ๋ค์ ๊ตฌ์กฐ๋ ๊ฐ๋ฅํ๋ค.

Spring Authorization Server๋
Keycloak๋ณด๋ค ๊ตฌ์ถ์ ์ด๋ ต์ง๋ง, ๋ณด์ ์ ์ด๊ถ๊ณผ ํ์ฅ์ฑ์ด ์๋์ ์ด๋ค.
โ ์์ ํ ๋ด๋ถ ํต์ (๋ณด์์ ์ฑ , Token ๊ตฌ์กฐ, Claim ์ค๊ณ)
โ Spring Security์ 100% ํธํ
โ Code-level ํ์ฅ (OAuth2TokenCustomizer, ConsentService ๋ฑ)
โ JWK / JWT / OIDC ์์ ๊ตฌํ
| ๊ตฌ๋ถ | Keycloak | Spring Authorization Server |
|---|---|---|
| ๋์ ๋์ด๋ | ๋ฎ์ | ์ค๊ฐ~๋์ |
| ํ์ฅ์ฑ | ์ ํ์ (์ค์ ์์ฃผ) | ์ฝ๋ ํ์ฅ ๋ฌดํ |
| ์ ์ง๋ณด์ | ์ ๋ฐ์ดํธ ์์กด | ์ง์ ๋ฒ์ ๊ด๋ฆฌ |
| ํด๋ผ์ฐ๋ ์ฐ๋ | Kubernetes ์ต์ ํ | Spring Cloud ์ฐ๊ณ ์ฉ์ด |
| ์ปค์คํฐ๋ง์ด์ง | Admin Console | Bean / Configurable Components |