gRPC 서비스에서 메타데이터로 인증 처리하기
배경
기존 RESTful API 일부를 gRPC로 전환하는 작업을 진행 중이다. 마이크로서비스 간 통신 성능 개선이 목적인데, 인증 처리를 어떻게 가져갈지 고민이 필요했다.
REST에서는 Authorization 헤더로 JWT를 전달했는데, gRPC에서는 메타데이터(Metadata)를 사용해야 한다.
클라이언트에서 메타데이터 전송
const grpc = require('grpc');
const metadata = new grpc.Metadata();
metadata.add('authorization', `Bearer ${token}`);
client.getUserInfo(request, metadata, (err, response) => {
if (err) console.error(err);
console.log(response);
});
헤더 대신 메타데이터 객체를 생성해서 두 번째 인자로 전달한다.
서버 인터셉터로 인증 검증
모든 RPC 호출마다 인증 로직을 넣을 순 없어서 인터셉터를 구현했다.
function authInterceptor(call, callback) {
const metadata = call.metadata.getMap();
const token = metadata.authorization?.replace('Bearer ', '');
if (!token) {
return callback({
code: grpc.status.UNAUTHENTICATED,
message: 'Token required'
});
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
call.request.userId = decoded.userId;
callback(null);
} catch (err) {
callback({
code: grpc.status.UNAUTHENTICATED,
message: 'Invalid token'
});
}
}
검증된 사용자 정보를 request에 추가해서 핸들러에서 접근 가능하게 했다.
인터셉터 등록
const server = new grpc.Server();
server.addService(UserService, {
getUserInfo: [authInterceptor, getUserInfoHandler]
});
배열로 미들웨어처럼 체이닝할 수 있다. 공개 API는 인터셉터를 제외하고 등록하면 된다.
소회
REST의 미들웨어 패턴과 크게 다르지 않았다. 다만 gRPC의 에러 코드 체계(grpc.status)를 사용해야 클라이언트에서 일관되게 처리할 수 있다는 점이 달랐다.
성능은 체감상 30% 정도 개선된 것 같은데, 정확한 벤치마크는 별도로 진행할 예정이다.