NestJS 마이크로서비스 #24 — User 로직을 Gateway로 옮기기 (Auth → Gateway & Guard)
목표
토큰 검증은 더 이상 각 마이크로서비스에서 직접 하지 않고 Gateway에서 통합 수행.
Gateway가 User 마이크로서비스에 토큰을 검증 요청 → 검증 통과 시 req.user에 페이로드(UserPayload) 저장.
컨트롤러에서는 가드(TokenGuard) 와 커스텀 데코레이터(@userPayload) 로 사용자 정보를 안전하게 획득.
Gateway가 Order 마이크로서비스 호출 시, meta.user 형태로 인증된 사용자 컨텍스트를 전달.
전체 구조 한눈에 보기
[Client] --HTTP(Authorization: Bearer xxx)--> [Gateway] ① BearerTokenMiddleware: 토큰 추출/검증 (User MS에 위임) ② req.user 주입 ③ TokenGuard: req.user 존재 확인 ④ Controller 핸들러: @userPayload()로 user 가져옴 ⑤ Gateway → Order MS: create_order(cmd) 요청 + meta.user 포함 ⑥ Order MS: meta.user.sub로 User 정보 조회, 주문 생성/결제 진행
1) Gateway 환경 및 마이크로서비스 클라이언트 설정
Gateway에서 User/Product/Order 서비스에 TCP로 연결합니다. 환경변수를 Joi로 검증하여 누락/오입력 시 부팅 단계에서 실패하도록 합니다.
// app.module.ts (요지)
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
HTTP_PORT: Joi.number().required(),
USER_HOST: Joi.string().required(),
USER_TCP_PORT: Joi.number().required(),
PRODUCT_HOST: Joi.string().required(),
PRODUCT_TCP_PORT: Joi.number().required(),
ORDER_HOST: Joi.string().required(),
ORDER_TCP_PORT: Joi.number().required(),
}),
}),
ClientsModule.registerAsync({
isGlobal: true,
clients: [
{ name: USER_SERVICE, useFactory: (cfg) => ({ transport: Transport.TCP, options: { host: cfg.get('USER_HOST'), port: Number(cfg.get('USER_TCP_PORT')) } }), inject: [ConfigService] },
{ name: PRODUCT_SERVICE, useFactory: (cfg) => ({ transport: Transport.TCP, options: { host: cfg.get('PRODUCT_HOST'), port: Number(cfg.get('PRODUCT_TCP_PORT')) } }), inject: [ConfigService] },
{ name: ORDER_SERVICE, useFactory: (cfg) => ({ transport: Transport.TCP, options: { host: cfg.get('ORDER_HOST'), port: Number(cfg.get('ORDER_TCP_PORT')) } }), inject: [ConfigService] },
],
}),
]
.env 예시
HTTP_PORT=3000 USER_HOST=localhost USER_TCP_PORT=4001 PRODUCT_HOST=localhost PRODUCT_TCP_PORT=4002 ORDER_HOST=localhost ORDER_TCP_PORT=4003
포인트: ConfigService.getOrThrow<number>를 쓰더라도 실제 반환은 문자열일 수 있으므로 Number(...) 캐스팅을 권장합니다.
2) Bearer 토큰 미들웨어 (Gateway)
Gateway에서 모든 /order 경로에 미들웨어를 적용하여,
Authorization: Bearer <JWT> 헤더에서 토큰 추출
User MS에 { cmd: 'parse_bearer_token' }로 토큰 파싱/검증 위임
성공 시 req.user = payload
// bearer-token.middleware.ts (개선 버전)
@Injectable()
export class BearerTokenMiddleware implements NestMiddleware {
constructor(@Inject(USER_SERVICE) private readonly userMS: ClientProxy) {}
async use(req: any, _res: any, next: (err?: any) => void) {
const token = this.extractBearer(req.headers?.authorization);
if (!token) return next(); // 비인증 요청 허용(가드에서 차단)
const result = await lastValueFrom(
this.userMS.send({ cmd: 'parse_bearer_token' }, { token })
);
if (result?.status === 'error') {
// 미들웨어에서 바로 던지면 전역필터에서 처리
throw new UnauthorizedException('유효하지 않은 토큰입니다.');
}
req.user = result.data; // { sub, type, iat, exp }
next();
}
private extractBearer(raw?: string | string[]): string | null {
if (!raw) return null;
const header = Array.isArray(raw) ? raw[0] : raw;
const [scheme, value] = header.split(' ');
if (!scheme || !value) return null;
return scheme.toLowerCase() === 'bearer' ? value.trim() : null;
}
}
적용
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(BearerTokenMiddleware).forRoutes('order');
}
}
왜 미들웨어인가? — 가드 이전 단계에서 req.user를 만들어 두면, 이후 여러 가드/인터셉터/핸들러에서 공통 컨텍스트로 재사용이 간편합니다.
3) TokenGuard — 인증 필수 라우트 보호
req.user가 존재하는지 확인합니다. 미들웨어에서 주입에 실패했거나 헤더가 없으면 401로 차단됩니다.
export class TokenGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
return !!req.user;
}
}
4) @userPayload 커스텀 데코레이터 — 사용자 페이로드 추출
핸들러에서는 아래처럼 간결하게 사용자 정보를 받을 수 있습니다.
export const userPayload = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const { user } = ctx.switchToHttp().getRequest();
if (!user) {
// 500(InternalServerError) 대신 401(Unauthorized)이 더 적절
throw new UnauthorizedException('인증 정보가 없습니다.');
}
return user; // { sub, type, iat, exp }
});
5) Gateway 컨트롤러/서비스 — Order 생성 요청
컨트롤러는 가드로 보호하고, 데코레이터로 받은 userPayload와 바디를 서비스로 전달합니다.
// gateway/apps/gateway/src/order/order.controller.ts
@Controller('order')
export class OrderController {
constructor(private readonly orderService: OrderService) {}
@Post()
@UseGuards(TokenGuard)
async createOrder(
@userPayload() user: UserPayloadDto,
@Body() dto: CreateOrderDto,
) {
return this.orderService.createOrder(dto, user);
}
}
Gateway 서비스에서는 Order MS에 RPC로 create_order를 요청할 때, **meta.user**에 인증 정보를 넣어 전달합니다.
// gateway/apps/gateway/src/order/order.service.ts
async createOrder(dto: CreateOrderDto, user: UserPayloadDto) {
return this.orderMS.send(
{ cmd: 'create_order' },
{ ...dto, meta: { user } }, // 핵심: 인증 컨텍스트 전달
);
}
요청 바디에는 토큰을 넣지 않습니다. 클라이언트는 헤더에만 토큰을 담고, Gateway가 이를 해석해 meta.user로 넘기는 것이 새 구조의 핵심입니다.
6) Order 마이크로서비스 변경점
기존에는 Order MS가 직접 토큰을 파싱했을 수 있습니다. 이제는 Gateway가 인증을 책임지므로, Order MS는 meta.user.sub만 사용해 User 정보를 조회하면 됩니다.
DTO 동기화
// apps/order/src/order/dto/create-order.dto.ts (핵심 필드)
export class CreateOrderDto implements UserMeta {
@ValidateNested()
@IsNotEmpty()
meta: { user: UserPayloadDto };
@IsArray()
@IsString({ each: true })
@IsNotEmpty({ each: true })
productIds: string[];
@ValidateNested()
@Type(() => AddressDto)
@IsNotEmpty()
address: AddressDto;
@ValidateNested()
@Type(() => PaymentDto)
@IsNotEmpty()
payment: PaymentDto;
}
불필요한 token 필드는 제거하세요. 이제 사용하지 않습니다.
서비스 로직 핵심
async createOrder(dto: CreateOrderDto) {
const { productIds, address, payment, meta } = dto;
// 1) 사용자 정보
const user = await this.getUserFromId(meta.user.sub);
// 2) 상품 정보
const products = await this.getProductsByIds(productIds);
// 3) 금액 계산 & 검증
const totalAmount = this.sum(products);
this.assertAmount(totalAmount, payment.amount);
// 4) 주문 생성 → 5) 결제 시도 → 6) 결과 업데이트
const order = await this.createNewOrder(user, products, address, payment);
const result = await this.processPayment(order._id.toString(), payment, user.email);
return this.orderModel.findById(order._id);
}
7) 요청–응답 시퀀스 (요약)
클라이언트: POST /order + Authorization: Bearer <JWT> 헤더 + 주문 바디 전송
Gateway 미들웨어: 토큰 파싱(→ User MS) 성공 시 req.user 주입
TokenGuard: req.user 없으면 401
컨트롤러: @userPayload()로 사용자 페이로드 받음
Gateway → Order MS: { cmd: 'create_order' }+ 바디 + meta.user
Order MS: meta.user.sub 기반 User 정보 조회 → 상품 조회/검증 → 결제 시도 → 주문 반환
8) 유효성 검증 & 보안 포인트
미들웨어에서 정확히 Bearer 스킴만 허용하고, 그 외 포맷은 401 처리.
@userPayload에서 401(Unauthorized) 를 사용(500 아님).
DTO는 공통 패키지(@app/common) 로 공유하면 타입 불일치 위험 감소.
UserPayloadDto의 iat/exp는 number 타입 권장(Unix time).
결제 금액은 서버 계산(total)과 프론트 입력(amount)을 반드시 비교.
9) 흔한 실수 & 개선(오류 정정)
Authorization 전체 문자열을 그대로 토큰으로 사용 ➜ Bearer 분리 파싱 필요.
데코레이터에서 InternalServerErrorException 사용 ➜ UnauthorizedException 으로 교체.
Order MS의 CreateOrderDto에 token 필드 남아있음 ➜ 제거.
PaymentMethod는 @IsEnum(PaymentMethod)로 검증 강화.
ConfigService에서 포트가 문자열로 들어오는 문제 ➜ Number(...)로 변환.
미들웨어 적용 경로가 잘못되어 인증이 누락 ➜ forRoutes('order')(또는 필요한 범위) 재확인.
10) 요청 예제 (테스트)
HTTP 요청
POST /order
Authorization: Bearer <JWT>
Content-Type: application/json
{
"productIds": ["8cc2...","39a9..."],
"address": { "name":"홍길동","street":"도산대로 14","city":"서울","postalCode":"123123","country":"대한민국" },
"payment": { "paymentMethod":"CreditCard", "paymentName":"법인카드", "cardNumber":"123123123123", "expiryYear":"26", "expiryMonth":"12", "birthOrRegistration":"9", "passwordTwoDigits":"12", "amount":3000 }
}
기대 동작
토큰 유효 ➜ req.user 주입 ➜ 가드 통과 ➜ Order MS에 meta.user 포함 전달 ➜ 주문/결제 처리.
토큰 누락/무효 ➜ 401 응답.
11) 미들웨어 vs 가드 — 언제 무엇을 쓸까?
미들웨어: 요청 가공/전처리(토큰 파싱, 로깅, 트레이싱). 라우트 핸들러와 무관한 공통 관심사 처리에 적합.
가드: 권한 부여(Authorization). 핸들러 실행 여부 자체를 결정.
본 사례: 토큰 파싱은 미들웨어, 인증 필수 여부는 가드가 담당.
이번 단계의 핵심은 인증의 단일화입니다.
Gateway에서 인증을 책임지고, 서비스 간에는 신뢰 가능한 meta.user 컨텍스트만 전달하세요.
이렇게 하면 각 마이크로서비스는 비즈니스 로직에 집중할 수 있고, 보안·정책 변경도 Gateway에서 일괄 반영할 수 있습니다.













댓글 ( 0)
댓글 남기기