처음에 이 코드를 사용했는데 일부 테스트케이스에서 실패를 했다.
class Solution {
public long solution(int price, int money, int count) {
for(int i = 1; i <= count; i++) {
money -= price * i;
}
return money < 0 ? -1*money : 0;
}
}

원본 잔액인 money를 훼손하고 또 계산 과정에 오버플로우가 날 수 있을 것 같아 long 변수로 뺐더니 테스트케이스는 통과되었다.
(괜히 solution 반환 타입이 long 타입인게 아니었구나)
class Solution {
public long solution(int price, int money, int count) {
long answer = money;
for(int i = 1; i <= count; i++) {
answer -= price * i;
}
return answer < 0 ? -1*answer : 0 ;
}
}
count 만큼 놀이기구를 타는 비용을 먼저 계산해서 빼는 방법도 있다.
이 경우 등차수열의 합 공식을 활용하면 반복문 없이도 값을 구할 수 있다.
(연산 식에서 long 타입 캐스팅을 해야 함에 주의하자)
class Solution {
public long solution(int price, int money, int count) {
long total = (long) price * count * (count + 1) / 2;
return total > money ? total - money : 0;
}
}
class Solution {
public boolean solution(String s) {
if(s.length() != 4 && s.length() != 6) return false;
for(char c : s.toCharArray()){
if(!(c>=48 && c<=57)) return false;
}
return true;
}
}
class Solution {
public boolean solution(String s) {
if(s.length() != 4 && s.length() != 6) return false;
for(char c : s.toCharArray()){
if(!Character.isDigit(c)) return false;
}
return true;
}
}
숫자 4개, 숫자 6개 정규식으로 풀어낼 수도 있다.
class Solution {
public boolean solution(String s) {
return s.matches("\\d{4}|\\d{6}");
}
}
API Gateway란 클라이언트의 요청을 받아 백엔드 서비스로 라우팅하고, 다양한 부가 기능을 제공하는 중간 서버이다.
API Gateway는 클라이언트와 서비스 간의 단일 진입점 역할을 하며 단순 전달 및 보안, 로깅, 모니터링, 요청 필터링 등의 공통 기능을 처리한다.
MSA 구조에서 Gateway를 사용하지 않으면 다음과 같은 문제가 있다.
각 서비스는 자기 도메인 책임만 가지게 하며
클라이언트는 Gateway만 알고 요청을 하고
Gateway가 각 마이크로서비스로 요청을 전달하도록 한다.
Client
│
▼
API Gateway
├── User Service
├── Order Service
└── Payment Service
계층이 다르다.
Client
│
▼
Internet Router
│
▼
Load Balancer
│
▼
API Gateway
│
├── User Service
├── Order Service
└── Payment Service
클라이언트 요청을 적절한 서비스로 전달
요청의 인증 및 권한을 검증
여러 서비스 인스턴스 간의 부하 분산
요청 및 응답을 로깅하고 모니터링
요청과 응답을 변환하거나 필터링
Spring Cloud Gateway는 Spring 프로젝트의 일환으로 개발된 API 게이트웨이로, 클라이언트 요청을 적절한 서비스로 라우팅하고 다양한 필터링 기능을 제공한다.
Spring Cloud Netflix 패키지의 일부로, 마이크로서비스 아키텍처에서 널리 사용된다.
Global Filter: 모든 요청에 대해 작동하는 필터
Gateway Filter: 특정 라우트에만 적용되는 필터
GlobalFilter 또는 GatewayFilter 인터페이스를 구현하고, filter 메서드를 오버라이드한다.
Mono
리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리
Mono<Void>는 아무 데이터도 반환하지 않음을 의미
ServerWebExchange
HTTP 요청과 응답을 캡슐화한 객체
exchange.getRequest()로 HTTP 요청을 가져옴
exchange.getResponse()로 HTTP 응답을 가져옴
GatewayFilterChain
여러 필터를 체인처럼 연결
chain.filter(exchange)는 다음 필터로 요청을 전달
then 메서드를 사용할 필요가 없음)
@Component
public class PreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 요청 로깅
System.out.println("Request: " + exchange.getRequest().getPath());
return chain.filter(exchange);
}
@Override
public int getOrder() { // 필터의 순서를 지정합니다.
return -1; // 필터 순서를 가장 높은 우선 순위로 설정합니다.
}
}
chain.filter(exchange)를 호출하여 다음 필터를 실행한 후, then 메서드를 사용하여 응답이 완료된 후에 실행할 작업을 정의하게 된다.
@Component
public class PostFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 응답 로깅
System.out.println("Response Status: " + exchange.getResponse().getStatusCode());
}));
}
@Override
public int getOrder() {
return -1;
}
}
Spring Cloud Gateway는 Reactive 기반(WebFlux) 이기 때문에 보통은 Reactive Gateway 의존을 사용한다.

Client
│
▼
API Gateway
(spring-cloud-starter-gateway)
(spring-cloud-starter-gateway-server-webmvc)
│
│ 서비스 위치 조회
▼
Service Discovery
(spring-cloud-starter-netflix-eureka-client)
Eureka
/ | \
▼ ▼ ▼
User Service Order Service Pay Service
(spring-boot-starter-web) (spring-boot-starter-web)
(spring-boot-starter-actuator) (spring-boot-starter-actuator)
실습에서는 서블릿 기반
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
routes:
- id: users-service # 라우트 식별자
uri: lb://users-service # 'users-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/users/** # /users/** 경로로 들어오는 요청을 이 라우트로 처리
- id: orders-service # 라우트 식별자
uri: lb://orders-service # 'orders-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/orders/** #/orders/** 경로로 들어오는 요청을 이 라우트로 처리
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
| Spring Gateway 실습 | 기존 로드밸런서 실습 |
|---|---|
19091 port: order -> gateway 나는 설정을 최소화하기 위해 gateway port를 18091로 설정해 추가해주기로 했다. | ![]() |
기존 product의 요청을 보내던 코드를 없애준다.
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping("/order/{orderId}")
public String getOrderWithProduct(@PathVariable String orderId) {
return orderService.getOrder(orderId);
}
@GetMapping("/order")
public String getOrder() {
return "Order dtail";
}
}
Product id를 받지 않게 한다. (괴랄한 이름이 되었다.)
@RestController
public class ProductController {
@Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
private String serverPort;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "Product " + id + " info!!!!! From port : " + serverPort ;
}
@GetMapping("product")
public String getProductWithOutProduct() {
return "Product info!!!! From port : " + serverPort;
}
}






import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest response = exchange.getRequest();
logger.info("Pre Filter: Request URI is " + response.getURI());
// Add any custom logic here
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter: Response status code is " + response.getStatusCode());
// Add any custom logic here
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
기존의 서블릿 필터와 구현 방식이 비슷해 보여 한 번 비교하고 넘어가기로 했다.
@Component
public class CustomServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. PRE 처리 (요청 전 로직)
System.out.println("Servlet Filter: Request logic");
// 2. 다음 필터 또는 서블릿으로 전달 (동기 방식)
chain.doFilter(request, response);
// 3. POST 처리 (응답 후 로직)
System.out.println("Servlet Filter: Response logic");
}
}
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. PRE 처리 (요청 전 로직)
System.out.println("Gateway Filter: Request logic");
// 2. 비동기 체인 실행 및 POST 처리 (응답 후 로직)
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("Gateway Filter: Response logic");
}));
}
@Override
public int getOrder() {
return -1; // 필터 실행 순서 지정
}
}
두 필터의 가장 큰 차이는 목적에 따른 실행 위치와 동기 / 비동기 차이라고 볼 수 있다.
| 구분 | Servlet 필터 (Spring MVC) | SCG 필터 (Spring Cloud Gateway) |
|---|---|---|
| 기반 엔진 | Tomcat (서블릿 컨테이너) | Netty (비동기 이벤트 루프) |
| 처리 모델 | 동기 & 블로킹 (Thread-per-request) | 비동기 & 논블로킹 (Event Loop) |
| 반환 타입 | void (명령형 스타일) | Mono<Void> (리액티브 스타일) |
| 핵심 객체 | HttpServletRequest / Response | ServerWebExchange |
| 필터 연결 | chain.doFilter() (직접 호출) | chain.filter().then() (체이닝) |
| 실행 위치 | 개별 애플리케이션 서비스 내부 | 마이크로서비스 최전방 (진입점) |
| 주요 역할 | 인코딩, 데이터 검증, 로컬 인증 | 라우팅, 로드밸런싱, 전역 공통 보안 |
| 예외 처리 | try-catch / ExceptionResolver | 리액티브 연산자 / WebExceptionHandler |
server:
port: 18091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/order/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/product/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정


Spring Cloud Gateway가 업데이트되면서, Servlet(MVC) 기반 게이트웨이와 WebFlux(비동기) 기반 게이트웨이의 설정을 더 명확히 구분하기 시작했다고 한다.
web-application-type: reactive를 사용 중이므로, server.webflux하위로 설정을 모으는 최신 스펙에 맞게 강의 예제 설정 파일을 바꿔주었다.
server:
port: 18091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
server:
webflux:
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/order/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/product/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
Gateway의 주소 하나로 분리된 서비스 요청에 대한 응답을 받아 올 수 있다.





OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜이다.
클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 한다.
리소스 소유자, 클라이언트, 리소스 서버, 인증 서버

| OAuth2 역할 | 그림에서의 명칭 | 설명 |
|---|---|---|
| 리소스 소유자 (Resource Owner) | User | 데이터의 실소유자입니다. (예: 구글 계정을 가진 사용자 본인) |
| 클라이언트 (Client) | Your App | 사용자의 권한을 빌려 리소스(데이터)를 이용하려는 어플리케이션입니다. |
| 인증 서버 (Authorization Server) | Google Servers (상단) | 사용자를 인증하고 클라이언트에게 권한 증표(Code, Token)를 발급하는 서버입니다. |
| 리소스 서버 (Resource Server) | Google Servers (하단) | Access Token을 확인하고 실제 사용자 데이터(프로필, 이메일 등)를 제공하는 서버입니다. |
Authorization Code Grant: 인증 코드를 사용하여 액세스 토큰을 얻는 방식
Implicit Grant: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
Resource Owner Password Credentials Grant: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
Client Credentials Grant: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식
JWT(JSON Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달한다.
JWT는 세 부분으로 구성: 헤더, 페이로드, 서명
JWT는 암호화를 통해 데이터의 무결성과 출처를 보장한다.

dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6' // jjwt 라이브러리
testImplementation 'io.projectreactor:reactor-test' // 비동기/논블로킹 스트림(Reactive Streams)을 테스트 도구
}
@Configuration
@EnableWebSecurity
public class AuthConfig {
// SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
.csrf(csrf -> csrf.disable())
// 요청에 대한 접근 권한을 설정합니다.
.authorizeRequests(authorize -> authorize
// /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증이 필요합니다.
.anyRequest().authenticated()
)
// 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 설정된 보안 필터 체인을 반환합니다.
return http.build();
}
}

// 요청에 대한 접근 권한을 설정합니다.
.authorizeHttpRequests(authorize -> authorize
// /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증이 필요합니다.
.anyRequest().authenticated()
)
예제의 메서드 이름을 authorizeRequests()가 authorizeHttpRequests()로 변경
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
@Service
public class AuthService {
@Value("${spring.application.name}")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
}
public String createAccessToken(String user_id) {
return Jwts.builder()
// 사용자 ID를 클레임으로 설정
.claim("user_id", user_id)
.claim("role", "ADMIN")
// JWT 발행자를 설정
.issuer(issuer)
// JWT 발행 시간을 현재 시간으로 설정
.issuedAt(new Date(System.currentTimeMillis()))
// JWT 만료 시간을 설정
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
// SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
.signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
// JWT 문자열로 컴팩트하게 변환
.compact();
}
}
public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
}
Decoders.BASE64URL.decode: 문자열로 된 비밀키를 컴퓨터가 읽을 수 있는 바이트 배열로 바꾼다.
Keys.hmacShaKeyFor: 이 바이트들을 사용해서 HMAC-SHA라는 암호화 알고리즘에 쓸 수 있는 진짜 '열쇠' 객체를 만듭니다. 이 열쇠가 있어야 토큰에 도장을 찍고(서명), 나중에 검증할 수 있습니다.
return Jwts.builder()
.claim("user_id", user_id) // "이 토큰 주인 ID는 이거야" (데이터)
.claim("role", "ADMIN") // "이 사람 권한은 관리자야" (데이터)
.issuer(issuer) // "누가 발급했어? 내가 했어"
.issuedAt(new Date(...)) // "언제 만들었어? 지금"
.expiration(new Date(...)) // "언제까지 써? 이때까지"
.signWith(secretKey, ...) // "내 비밀키로 도장 찍을게" (서명)
.compact(); // "다 쌓았으니 문자열로 합쳐줘!"
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
*
* @param user_id 사용자 ID
* @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
*/
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
/**
* JWT 액세스 토큰을 포함하는 응답 객체입니다.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
실습편의를 위해 Post대신 Get 매핑을 만든다.
ResponseEntity의 타입을? (wildcard) 타입으로 정해 어떤 응답이라도 타입에 관계 없이 ResponseEntity로 반환 가능하게 만든다.

auth서비스가 SiginIn 경로로 UserId를 담아 요청하면
JWT AccessToken을 발행해 줌을 볼 수 있다.
Gateway에서도 jwt를 활용할 수 있도록 의존성을 추가해준다.
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
Auth 서비스와 동일한 키 값을 가지도록 설정에 추가해준다.
service:
jwt:
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"

Auth 서비스에 대한 라우팅도 추가해준다.
이전 단계에서 AuthService가 통행증(JWT)을 발급해주는 곳이었다면, 이번 LocalJwtAuthenticationFilter는 게이트웨이 입구에서 그 통행증이 진짜인지 확인하는 곳이다.
GlobalFilter를 상속받았다는 것은, 게이트웨이로 들어오는 모든 요청이 이 클래스를 거쳐가야 한다는 뜻으로, 사용자가 상품 목록을 보든, 주문을 하든 상관없이 게이트웨이는 이 필터를 먼저 실행하여 "너, 통행증(JWT) 있어?"라고 물어본다.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (path.equals("/auth/signIn")) {
return chain.filter(exchange); // /signIn 경로는 필터를 적용하지 않음
}
String token = extractToken(exchange);
if (token == null || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/*
JWT는 HTTP 헤더의 Authorization 필드에 Bearer [토큰값] 형식으로 실려 옵니다.
이 메서드는 약속된 규격에서 실제 데이터인 토큰만 분리해내는 역할을 합니다.
*/
private String extractToken(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7); ; // "Bearer " 뒷부분인 토큰 값만 쏙 빼옴
}
return null;
}
private boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(key) // 서버들만이 가진 secretKey로 서버가 발행한 것인지 확인
.build().parseSignedClaims(token);
log.info("#####payload :: " + claimsJws.getPayload().toString());
// 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
return true;
} catch (Exception e) {
return false;
}
}
}
// 예시: 다음 서비스로 사용자 ID 전달하기
exchange.getRequest().mutate()
.header("X-User-Id", String.valueOf(claimsJws.getPayload().get("user_id")))
.build();
보통은 검증이 끝난 후 claimsJws에서 뽑아낸 user_id를 헤더에 다시 담아서 보내주는 로직을 추가하곤 합니다.





Gateway에서 인증을 사용하더라도 서비스에 직접 요청하면 인증 정보 없이도 응답을 받을 수 있다.
서비스는 게이트웨이의 요청만을 받도록 방화벽 처리를 해야한다.
서비스를 물리적으로 또는 가상 네트워크(VPC) 내부로 숨기는 방식
주로 클라우드(AWS 등) 환경에서 이 방식을 사용한다.
보안 그룹(Security Group) 설정:
- 개별 서비스(Order, Product 등): 자신의 포트(예: 19091)에 대해 Gateway의 IP 주소에서 오는 요청만 허용하도록 인바운드 규칙을 설정
- 그 외의 IP는 모두 차단(Drop)
Gateway:
- 외부 클라이언트(0.0.0.0/0)의 요청을 받을 수 있도록 포트(예: 18091)를 개방
모든 서비스가 게이트웨이가 보낸 '비밀 표식'을 확인하게 만드는 방식
모든 요청을 보낼 때 특정 헤더를 강제로 추가하도록 설정한다.
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=X-Gateway-Token, MySecretKey123
모든 API 요청에서 X-Gateway-Token 헤더가 MySecretKey123과 일치하는지 검사하는 Interceptor나 Filter를 구현한다.
일치하지 않으면 바로 403 Forbidden으로 처리한다.
개별 서비스에도 Spring Security를 적용하되, 게이트웨이와 같은 JWT 비밀키를 공유하게 하는 방법
동작 원리: 사용자가 게이트웨이를 거치지 않고 서비스 포트로 직접 요청을 보내더라도, 서비스 자체에 걸려있는 Security가 인증 토큰(JWT)을 검증한다.
장점: 게이트웨이를 통하든 직접 요청하든 보안이 유지된다.