이 글에서 얻는 것
- OAuth2 Authorization Code 흐름을 실제 Spring Security 설정으로 연결할 수 있습니다.
- JWT 발급/검증을 Resource Server 설정으로 구현할 수 있습니다.
- 실무에서 자주 터지는 토큰 저장/만료/리프레시 정책의 함정을 예방할 수 있습니다.
1) 전체 구조 한 장으로 이해하기
sequenceDiagram
participant User as 사용자
participant Client as 클라이언트(웹/앱)
participant AS as Authorization Server
participant RS as Resource Server(API)
User->>Client: 로그인 시도
Client->>AS: /authorize (Authorization Code 요청)
AS-->>Client: Authorization Code
Client->>AS: /token (Code -> Access Token 교환)
AS-->>Client: Access Token (+ Refresh Token)
Client->>RS: API 요청 (Authorization: Bearer <token>)
RS-->>Client: 응답
핵심은 Authorization Server(AS)에서 발급한 JWT를 Resource Server(RS)에서 검증하는 구조입니다.
2) Authorization Server 구성 (Spring Authorization Server)
실제 운영에서는 AS와 RS를 분리하는 것이 일반적입니다.
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret(encoder.encode("secret"))
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("https://app.example.com/login/oauth2/code")
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder().requireProofKey(true).build()) // PKCE
.build();
return new InMemoryRegisteredClientRepository(client);
}
requireProofKey(true)로 PKCE 활성화- Access Token에 사용할 JWT 서명 키(JWK) 를 반드시 관리
3) Resource Server에서 JWT 검증
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://auth.example.com/.well-known/jwks.json")
)
);
return http.build();
}
- Resource Server는 JWK URL만 알면 서명 검증 가능
- DB 세션이 필요 없으므로 완전 무상태
4) JWT 클레임 설계 포인트
{
"sub": "user-123",
"roles": ["ROLE_USER"],
"scope": "read write",
"iat": 1710000000,
"exp": 1710003600
}
sub= 사용자 식별자roles/scope= 인가 기준exp= 만료 시간을 반드시 짧게
실무 팁: 짧은 Access Token + Refresh Token 조합이 안정적입니다.
5) Refresh Token 전략 (실무 함정)
5-1. 저장 위치
- 웹: HttpOnly Secure Cookie 권장
- 모바일: Keychain/Keystore 저장
5-2. Rotation
Refresh Token을 매번 재발급하는 Rotation 방식을 쓰면 탈취 대응이 유리합니다.
public TokenPair rotate(String refreshToken) {
validate(refreshToken); // 블랙리스트/만료 확인
revoke(refreshToken); // 기존 토큰 폐기
return issueNewTokens();
}
5-3. 로그아웃
- Access Token은 만료가 짧아 즉시 무효화가 어렵습니다.
- Refresh Token 블랙리스트 + Access Token 만료 단축으로 대응합니다.
6) 흔한 장애 시나리오와 대응
- JWT 검증 실패: JWK 캐시 갱신 실패 → JWK URL health check
- 만료 오류 급증: 서버/클라이언트 시간 불일치 → NTP 동기화
- 로그아웃 후 접근 가능: Access Token 만료가 너무 길다 → 5~15분 권장
요약
- OAuth2의 핵심은 Authorization Server 발급 + Resource Server 검증 분리
- JWT는 짧은 만료 + Refresh Token Rotation이 기본 전략
- Spring Security의 Resource Server 설정으로 JWT 검증을 간결하게 구현 가능
연습(추천)
- 로컬에서 Authorization Server/Resource Server를 분리해 띄워보고 JWK 검증 로그 확인
- Access Token 만료 시간을 1분으로 설정하고 Refresh 흐름을 실제로 테스트해보기
- JWT 클레임에
roles를 넣고@PreAuthorize로 인가 규칙을 만들어보기
💬 댓글