Spring Authorization Server란?
Spring Authorization Server는 Spring 팀이 공식 지원하는 OAuth 2.1 / OpenID Connect 1.0 인가 서버 프레임워크입니다. 기존 Spring Security OAuth 프로젝트가 2022년 EOL된 이후, 완전히 새로 설계된 후속 프로젝트로 Spring Boot 3.x와 완벽히 호환됩니다. Keycloak 같은 외부 IdP 없이 자체 인가 서버를 Spring 생태계 안에서 구축할 수 있습니다.
핵심 아키텍처
Spring Authorization Server는 다음 네 가지 핵심 컴포넌트로 구성됩니다.
| 컴포넌트 | 역할 | 커스터마이징 포인트 |
|---|---|---|
| RegisteredClientRepository | OAuth2 클라이언트 등록·조회 | JPA 영속화, 동적 등록 |
| OAuth2AuthorizationService | 인가 정보(코드·토큰) 저장 | Redis/DB 백엔드 |
| OAuth2TokenGenerator | 액세스·리프레시·ID 토큰 생성 | 커스텀 클레임, 토큰 포맷 |
| OAuth2TokenCustomizer | 토큰 클레임 커스터마이징 | 역할·권한·프로필 추가 |
기본 설정: Authorization Server 구성
Spring Boot 3.x 기반 최소 설정부터 시작합니다.
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
}
// application.yml
spring:
security:
oauth2:
authorizationserver:
issuer-url: https://auth.example.com
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // OpenID Connect 활성화
http.exceptionHandling(e -> e
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsaKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return (selector, context) -> selector.select(jwkSet);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("https://auth.example.com")
.build();
}
}
클라이언트 등록: JPA 영속화
프로덕션에서는 인메모리 대신 DB 기반 클라이언트 관리가 필수입니다.
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// Spring 제공 JDBC 구현체
JdbcRegisteredClientRepository repository =
new JdbcRegisteredClientRepository(jdbcTemplate);
// 초기 클라이언트 등록
RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-app")
.clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("https://app.example.com/callback")
.postLogoutRedirectUri("https://app.example.com/logout")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("api.read")
.scope("api.write")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true) // 동의 화면 표시
.requireProofKey(true) // PKCE 강제
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofDays(7))
.reuseRefreshTokens(false) // Refresh Token Rotation
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.build())
.build();
repository.save(webClient);
return repository;
}
핵심 포인트:
requireProofKey(true): OAuth 2.1 권장 사항인 PKCE를 강제하여 Authorization Code 가로채기 공격을 방지합니다.reuseRefreshTokens(false): Refresh Token Rotation으로 토큰 탈취 시 피해를 최소화합니다.- 비밀번호는 반드시 BCrypt 인코딩으로 저장합니다.
커스텀 JWT 클레임 추가
액세스 토큰과 ID 토큰에 사용자 역할, 권한 등 커스텀 클레임을 주입하는 방법입니다.
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(
UserService userService) {
return context -> {
// 액세스 토큰 커스터마이징
if (context.getTokenType() == OAuth2TokenType.ACCESS_TOKEN) {
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims()
.claim("roles", authorities)
.claim("tenant_id", extractTenantId(principal));
}
// ID 토큰 커스터마이징 (OIDC)
if (context.getTokenType().getValue().equals("id_token")) {
UserProfile profile = userService
.findByUsername(context.getPrincipal().getName());
context.getClaims()
.claim("picture", profile.getAvatarUrl())
.claim("locale", profile.getLocale())
.claim("email_verified", profile.isEmailVerified());
}
};
}
Resource Server에서는 이 커스텀 클레임을 기반으로 세밀한 접근 제어를 구현할 수 있습니다. @PreAuthorize("hasAuthority('SCOPE_api.write') and #jwt.claims['tenant_id'] == #tenantId") 같은 SpEL 표현식이 가능해집니다.
PKCE + Authorization Code Flow 전체 흐름
OAuth 2.1에서 권장하는 PKCE 기반 Authorization Code Grant의 전체 시퀀스입니다.
// 1. 클라이언트: code_verifier 생성 + code_challenge 계산
String codeVerifier = generateRandomString(128);
String codeChallenge = Base64.urlEncode(SHA256(codeVerifier));
// 2. 인가 요청 (브라우저 리다이렉트)
GET /oauth2/authorize?
response_type=code
&client_id=web-app
&redirect_uri=https://app.example.com/callback
&scope=openid profile api.read
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw
&code_challenge_method=S256
&state=xyz123
// 3. 사용자 로그인 + 동의 → 인가 코드 발급
302 Location: https://app.example.com/callback?code=AUTH_CODE&state=xyz123
// 4. 토큰 교환 (서버 사이드)
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(web-app:secret)
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://app.example.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// 5. 응답: access_token + refresh_token + id_token
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"refresh_token": "Atzr|IQEBLzAtA...",
"id_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 1800
}
Machine-to-Machine: Client Credentials
서비스 간 통신에는 Client Credentials Grant를 사용합니다.
// 서비스 전용 클라이언트 등록
RegisteredClient serviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("payment-service")
.clientSecret("{bcrypt}" + encoder.encode("service-secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("internal.payment.process")
.scope("internal.order.read")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(10)) // 짧은 TTL
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.build())
.build();
// 서비스에서 토큰 요청
POST /oauth2/token
Authorization: Basic base64(payment-service:service-secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=internal.payment.process
M2M 토큰은 TTL을 짧게(10분 이내) 설정하고, scope를 최소 권한으로 제한하는 것이 보안 모범 사례입니다.
인가 정보 DB 저장: JdbcOAuth2AuthorizationService
다중 인스턴스 환경에서는 인가 정보를 DB에 저장해야 합니다.
@Bean
public OAuth2AuthorizationService authorizationService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository clientRepository) {
JdbcOAuth2AuthorizationService service =
new JdbcOAuth2AuthorizationService(jdbcTemplate, clientRepository);
// 커스텀 Row Mapper (추가 메타데이터 저장 시)
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(clientRepository);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new CoreJackson2Module());
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// 커스텀 타입 등록 (UserProfile 등)
objectMapper.addMixIn(UserProfile.class, UserProfileMixin.class);
rowMapper.setObjectMapper(objectMapper);
service.setAuthorizationRowMapper(rowMapper);
return service;
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository clientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, clientRepository);
}
Spring Authorization Server는 필요한 테이블 스키마를 공식 제공합니다. org/springframework/security/oauth2/server/authorization/ 경로에서 DDL을 확인할 수 있습니다.
토큰 무효화(Revocation)와 Introspection
Spring Authorization Server는 Token Revocation(RFC 7009)과 Token Introspection(RFC 7662) 엔드포인트를 기본 제공합니다.
// 토큰 무효화 요청
POST /oauth2/revoke
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJSUzI1NiJ9...&token_type_hint=access_token
// 토큰 검증 요청 (Opaque 토큰 사용 시)
POST /oauth2/introspect
Authorization: Basic base64(client_id:client_secret)
token=kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNn2DhB
// 응답
{
"active": true,
"sub": "user123",
"scope": "openid api.read",
"client_id": "web-app",
"exp": 1711350000
}
다중 IdP 연동: Social Login + 자체 인증
Google, GitHub 등 소셜 로그인을 Authorization Server의 인증 수단으로 통합하는 패턴입니다.
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/error").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login"))
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
));
return http.build();
}
// application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user, user:email
사용자는 폼 로그인 또는 Google/GitHub 중 선택하여 인증하고, Authorization Server는 통합된 OAuth2 토큰을 발급합니다. 하나의 인가 서버가 여러 인증 수단을 통합하는 허브 역할을 합니다.
프로덕션 보안 체크리스트
Spring Authorization Server를 프로덕션에 배포할 때 반드시 확인해야 할 보안 항목입니다.
- HTTPS 필수: 모든 엔드포인트에 TLS를 적용하고,
issuer-url도 HTTPS로 설정 - PKCE 강제: 모든 Authorization Code 클라이언트에
requireProofKey(true)설정 - Refresh Token Rotation:
reuseRefreshTokens(false)로 탈취 감지 가능 - 토큰 TTL 최소화: Access Token 15-30분, Refresh Token 7일 이내
- Client Secret 암호화: BCrypt 또는 Argon2로 인코딩하여 저장
- Redirect URI 정확 매칭: 와일드카드 사용 금지, 정확한 URI만 허용
- JWK 키 로테이션: RSA/EC 키를 주기적으로 교체하고,
kid로 식별 - Rate Limiting:
/oauth2/token엔드포인트에 요청 제한 적용
Keycloak 대비 장단점
| 항목 | Spring Authorization Server | Keycloak |
|---|---|---|
| 커스터마이징 | 코드 레벨 완전 제어 | SPI 확장 (제한적) |
| 관리 UI | 직접 구현 필요 | 내장 Admin Console |
| 멀티테넌시 | 직접 구현 | Realm 기반 내장 |
| 운영 복잡도 | Spring Boot 앱 하나 | 별도 인프라 필요 |
| 프로토콜 지원 | OAuth 2.1 + OIDC | OAuth 2.0 + OIDC + SAML |
| 적합한 상황 | Spring 기반 마이크로서비스 | 이기종 환경, 엔터프라이즈 |
Spring 생태계 중심의 마이크로서비스라면 Spring Authorization Server가 적합하고, SAML 지원이나 관리 UI가 즉시 필요하다면 Keycloak이 유리합니다.
Spring Authorization Server는 인가 로직을 애플리케이션 코드로 완전히 제어할 수 있어, 복잡한 비즈니스 요구사항에 유연하게 대응할 수 있습니다. OAuth 2.1 표준 준수와 Spring Security와의 긴밀한 통합이 최대 장점입니다. Resource Server 측 JWT 검증 설정은 Spring OAuth2 Resource Server 글을, 메서드 수준 보안은 Spring Method Security 심화 가이드를 참고하세요.