gRPC 서비스 간 인증 구현하기 - Metadata Interceptor 활용
문제 상황
회사 프로젝트에서 여러 마이크로서비스를 gRPC로 연결하면서, 서비스 간 인증 레이어가 필요해졌다. REST API처럼 간단히 헤더에 토큰을 넣는 방식이 gRPC에서는 어떻게 동작하는지 파악이 필요했다.
gRPC Metadata
gRPC에서는 HTTP 헤더와 유사한 개념으로 Metadata를 제공한다. 클라이언트에서 메타데이터를 추가하는 방식은 다음과 같다.
const grpc = require('grpc');
const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${token}`);
client.getUser({ userId: 123 }, metadata, (err, response) => {
// ...
});
Server Interceptor 구현
문제는 매번 메타데이터를 수동으로 추가하는 것이 번거롭다는 점이었다. Node.js gRPC 서버에서 인터셉터를 구현해 모든 요청을 검증하도록 했다.
function authInterceptor(call, callback) {
const metadata = call.metadata;
const authHeader = metadata.get('authorization')[0];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return callback({
code: grpc.status.UNAUTHENTICATED,
message: 'Missing or invalid token'
});
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, SECRET_KEY);
call.metadata.add('user-id', decoded.userId);
callback(null);
} catch (err) {
callback({
code: grpc.status.UNAUTHENTICATED,
message: 'Invalid token'
});
}
}
클라이언트 인터셉터
클라이언트 측에서도 매번 토큰을 추가하는 대신, 인터셉터로 자동화했다.
const grpc = require('grpc');
function createAuthInterceptor(token) {
return function(options, nextCall) {
return new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
metadata.add('authorization', `Bearer ${token}`);
next(metadata, listener);
}
});
};
}
const client = new proto.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
{ interceptors: [createAuthInterceptor(userToken)] }
);
결과
인터셉터 패턴을 활용하니 비즈니스 로직과 인증 로직이 분리되어 코드가 깔끔해졌다. REST API에서 미들웨어를 쓰던 것과 유사한 경험이었다. gRPC 특유의 메타데이터 개념만 이해하면 구현 자체는 어렵지 않았다.