리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)
단일 애플리케이션에서 모놀리식으로 인증/인가를 처리하다가 게이트웨이 + 마이크로서비스 구조로 바꾸면, 어디에서 인증을 검증하고 권한을 어떻게 전파할지가 핵심 이슈가 됩니다. 이 글은 Spring Cloud Gateway를 프런트 도어로 두고, 백엔드는 Resource Server(JWT)로 나누는 실전 패턴을 제공합니다.
1) 기본 전략: “게이트웨이도 검증, 백엔드도 검증”
- 게이트웨이:
Authorization: Bearer ...를 받아 JWT 유효성을 1차 검증(만료/서명/클레임). - 백엔드(리소스 서버): 게이트웨이를 통과한 요청이라도 다시 검증합니다. 서비스 간 직접 호출, 내부 노출 등 우발적 바이패스를 막습니다.
- 권한 전파:
Authorization헤더 그대로 전달(권장). 혹은X-Principal,X-Roles등 헤더로 파생 전파(민감, 신중).
2) 게이트웨이 설정: JWT 리소스 서버 + 라우팅
# application.yml (Gateway)
server:
port: 8080
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://auth.example.com/.well-known/jwks.json # 5편 구성 재사용
cloud:
gateway:
default-filters:
- RemoveRequestHeader=Cookie
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
routes:
- id: user-service
uri: http://user:8081
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1 # /api 제거 → /users/** 로 전달
- id: order-service
uri: http://order:8082
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
게이트웨이는 자체적으로 JWT를 검증만 하고, 권한 판단은 백엔드가 상세히 수행합니다. 공통 CORS/보안 헤더는 게이트웨이에서 통일하면 편합니다.
3) 백엔드 서비스: Spring Security Resource Server
# application.yml (User Service)
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://auth.example.com/.well-known/jwks.json
@Configuration
@EnableMethodSecurity
public class SecurityCfg {
@Bean
SecurityFilterChain http(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers(HttpMethod.GET, "/users/me").hasAnyRole("USER","ADMIN")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt());
return http.build();
}
}
@PreAuthorize로 도메인 권한까지 세밀 제어합니다. (7편의 권한 모델링 글 참고)
4) Token Relay(선택): 게이트웨이가 토큰을 갱신/주입
OAuth2 Client로 게이트웨이가 동작하며, 백엔드로 액세스 토큰을 릴레이하는 패턴도 있습니다. 그러나 대부분의 BFF/API 시나리오는 클라이언트 → 게이트웨이 → 백엔드로 동일 토큰을 그대로 전달하는 게 단순하고 안전합니다.
5) 401/403 처리 표준화
@Configuration
public class ExceptionHandlers {
@Bean
SecurityWebFilterChain gateway(HttpSecurity http) throws Exception {
http.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> {
res.setStatusCode(HttpStatus.UNAUTHORIZED);
})
.accessDeniedHandler((req, res, e) -> {
res.setStatusCode(HttpStatus.FORBIDDEN);
}));
return http.build();
}
}
클라이언트는 401 = 로그인 필요, 403 = 권한 없음으로 분기합니다. 메시지·코드 규격을 시리즈 전반에서 통일하세요.
6) CORS/보안 헤더는 게이트웨이에서 1곳 관리
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
"[/**]":
allowedOrigins: "https://app.example.com"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
백엔드들마다 따로 CORS를 두지 말고, 게이트웨이에서 단일 소스로 관리하는 것이 운영 안정적입니다.
7) 권장 토폴로지와 트러블슈팅
- HTTPS 종단: 외부는 게이트웨이에서 종단, 내부도 TLS 원칙 적용을 권장.
- 헤더 보존:
Authorization,Trace-Id,X-Request-Id보존. - 시간 동기화: JWT 만료/발급 스큐를 줄이려 모든 노드 NTP 동기화.
- 키 회전: 5편의 JWK 키 회전 정책을 게이트웨이/백엔드 모두 적용.
👉 1편: SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기
👉 2편: PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책
👉 3편: AuthenticationProvider & UserDetailsService 커스터마이징
👉 4편: 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리
👉 5편: JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략
👉 6편: OAuth2 로그인(Google/GitHub) + JWT 브릿지
👉 7편: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한
👉 8편: CORS·XSS·헤더 보안: SPA/REST 현실 설정
'Java & Spring' 카테고리의 다른 글
| REST API 에러 코드 규격: RFC 7807 Problem Details로 한 번에 정리(Spring Boot 실전) (0) | 2025.10.25 |
|---|---|
| REST API 에러 코드 규격: 문제 상세(Problem Details)로 정리 (0) | 2025.10.18 |
| CORS·XSS·헤더 보안: SPA/REST 현실 설정 (0) | 2025.10.14 |
| 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한 (0) | 2025.10.14 |
| OAuth2 로그인(Google/GitHub) + JWT 브릿지 (0) | 2025.10.13 |