gRPC на Go: Пишем микросервис аутентификации с нуля
- понедельник, 24 августа 2026 г. в 00:00:14
Привет, Хабр! Сегодня рассмотрим тему gRPC и работы с ним в контексте языка программирования Go на примере простейшей реализации аутентификации. Полный пример вы можете найти в архивном репозитории github.
gRPC является современным фреймворком, разработанным Google для связи между программными сервисами. RPC - это тип связи, который позволяет приложениям взаимодействовать друг с другом по сети. Он позволяет вызывать процедуры одного приложения из другого, что обеспечивает большую масштабируемость и позволяет реализовывать сложную микросервисную архитектуру. Также благодаря возможности версионирования и указания конкретной структуры API в файле Protobuf, появляется возможность поддерживать единое состояние у всех сервисов системы. Основным смыслом gRPC является то, что работа происходит в виде набора байт, а не JSON и тому подобных форматов, при этом также нужно учитывать, что gRPC - это не только бинарный формат, но и HTTP/2, так как именно HTTP/2 даёт мультиплексирование и стримминг. Это делает общение между сервисами максимально быстрым. Основным краеугольным камнем здесь является Protobuf.
Protobuf - это протокол сериализации (передачи) структурированных данных. Этот протокол эффективно хранит структурированные данные в двоичной форме, что позволяет быстро передавать их по сети.
Вообще, Protobuf - это универсальный протокол. Он может использоваться в коммуникационных протоколах, хранилищах данных и многом другом.
Основное отличие между Protobuf и JSON форматом в том, что JSON использует человекочитаемый формат обычного текста, а Protobuf кодирует данные в двоичном формате. Получается, что передача того же объёма информации требует меньшей пропускной способности, чем JSON.
На текущий момент последним вариантом Protobuf является proto3, в который добавили: тип map для хэш-карт, типы для работы с датой, временем и динамическими значениями, а также важный тип optional, который решает известную проблему присутствия полей. До этого, если передавать значение поля 0 для числовых или "" для строковых типов, то данные вообще не отправлялись, так как считались пустыми. На текущий момент тип optional позволяет указать присутствие поля.
Для работы Protobuf в контексте gRPC на Go необходимо написать файл .proto, который является схемой API. Важно учитывать, что .proto файл - это не просто какая-то документация, а строгие инструкции, которые являются исполняемым кодом, который, после компиляции protoc будет сгенерирован в код Go. Вот пример такого файла:
syntax = "proto3"; package auth; option go_package = "./auth"; //Сервис аутентификации: service Auth { rpc Login(LoginRequest) returns (LoginReply); rpc CheckAuth(CheckAuthRequest) returns (CheckAuthResponse); rpc Logout(LogoutRequest) returns (LogoutResponse); } // Запрос и ответ для функционала входа (Login): message LoginRequest { string login = 1; string password = 2; } message LoginReply { string token = 1; } // Запрос и ответ для функционала проверки входа/токена (CheckAuth): message CheckAuthRequest { string token = 1; } message CheckAuthResponse { bool is_auth = 1; string admin_id = 2; string login = 3; } // Запрос и ответ для функционала выхода из сессии (Logout): message LogoutRequest { string token = 1; } message LogoutResponse { bool is_logout = 1; }
Здесь можно заметить один сервис Auth и конкретные типы message. В сервисы указываются методы и к методам в дополнение указываются конкретные типы принимаемых и возвращаемых значений.
Детально разбирать установку protoc и генерацию не будем, так как это зависит от вашей ОС, используемых директорий. Главное, что следует указать, что для генерации необходимо:
Компилятор protoc.
Go-плагины, их можно установить с помощью этих команд:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Обновить переменные. можно сделать этой командой, если используете оболочку Bash:
export PATH="$PATH:$(go env GOPATH)/bin"
Также вот пример команды, с помощью которой происходит генерация:
protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ yourdir/yourfile.proto
После компиляции должна появиться ваша директория с двумя файлами:
your.pb.go - этот файл содержит описание данных. Здесь находятся Go-структуры, которые соответствуют message из .proto файла. Здесь же лежит логика сериализации и десериализации.
your_grpc.pb.go - этот файл содержит описание поведения. Здесь находятся интерфейсы сервера (то, что программисту нужно реализовать), находится клиентский код (функции, которые отправляют запросы по сети) и здесь же описаны сами RPC-методы.
Хотелось бы предостеречь вас. Нужно понимать, что эти файлы являются сгенерированными. Вручную их править ни в коем случае нельзя. Такие действия могут привести к очень печальным последствиям в виде расхождения с основным .proto файлом и ошибок компиляции.
Основой работы с gRPC является клиент-серверная архитектура. При этом клиентом здесь является не браузер или конечное устройство пользователя, а какой-либо другой сервер. К примеру, вполне может существовать подобная архитектура:

На схеме показан паттерн BFF, где браузер общается с веб-сервером по HTTP/JSON, а веб-сервер выступает адаптером к внутреннему gRPC-серверу.
Для работы с gRPC необходимо создать сервер. Нужно создать структуру и метод её создания для сервера. В подобной структуре может находиться все необходимые зависимости и то, что должно быть во всех обработчиках (к примеру пул подключения к базе данных), но главным в любом случае будет UnimplementedYourServer из файла _grpc.pb.go, что позволяет сделать структуру валидной для интерфейса, сгенерированного protoc. Ниже приведён пример подобной структуры:
type AuthServer struct { pb.UnimplementedAuthServer PostgresPool *pgxpool.Pool RedisClient *redis.Client JWTSecret string } func NewStruct() *AuthServer { return &AuthServer{} }
Ниже приведена функция запуска gRPC-сервера:
func StartServer(ctx context.Context, pool *pgxpool.Pool, redis *redis.Client, JWTSecret string) { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("Ошибка создания слушателя порта :50051: %v", err) } //Создаём экземпляр структуры сервера аутентификации: authServer := &internal.AuthServer{ PostgresPool: pool, RedisClient: redis, JWTSecret: JWTSecret, } grpcServer := grpc.NewServer() pb.RegisterAuthServer(grpcServer, authServer) go func() { log.Println("Запуск gRPC-сервера на порту :50051...") if err := grpcServer.Serve(lis); err != nil { log.Printf("Ошибка запуска gRPC-сервера: %v", err) } }() <-ctx.Done() log.Println("Получил сигнал завершения, начинаю shutdown...") grpcServer.GracefulStop() log.Println("gRPC-сервер успешно остановлен!") }
Сервер создаётся на порту 50051 - это стандартный порт для gRPC-сервера. Для примера реализации одного из методов (Logout) описанных в .proto ниже предоставлен код:
func (s AuthServer) Logout(ctx context.Context, req *pb.LogoutRequest) (*pb.LogoutResponse, error) { //Верифицируем токен и получаем его поля: jwtValues, err := auth.VerifyJWT(req.Token, s.JWTSecret) if err != nil { if errors.Is(err, auth.ErrInvalidToken) { return nil, status.Error(codes.Unauthenticated, "invalid token") } return nil, status.Error(codes.Internal, "token verification failed") } //Удаляем из Redis: err = repositories.DelRedisSession(ctx, jwtValues.SessionID, s.RedisClient) if err != nil { if err != redis.Nil { return nil, status.Error(codes.Internal, "Internal server error!") } } return &pb.LogoutResponse{IsLogout: true}, nil }
Синтаксис как и у любого другого метода Go, только тип принимаемого значение и возвращаемого в сигнатуре функции особенный. Как правило, сигнатуры берутся из файла _grpc.pb.go, а реализация наполнения остаётся за разработчиком. В случае ошибки в gRPC принято использовать status, о чём описано далее.
В привычном REST принято возвращать HTTP 404 или другие статусы ошибок, то в gRPC ошибки - это отдельный язык общения между сервисами. Для правильного построения архитектуры нужно использовать пакет google.golang.org/grpc/status и перечисление codes, чтобы клиент не потерял контекст ошибки. Вот основные коды, которые используются в gRPC:
OK - аналог 200, означает об успехе.
INVALID_ARGUMENT - аналог 400 Bad Request, означает, что клиент прислал не верные данные (пустой email или что-то подобное).
NOT_FOUND - аналог 404 Not Found, означает, что запрашиваемый ресурс не найден.
ALREADY_EXISTS - аналог 409 Conflict, означает попытку создать дубликат.
UNAUTHENTICATED - аналог 401 Unauthorized, нет токена или он протух.
PERMISSION_DENIED - аналог 403 Forbidden, токен есть, но права на действие нет.
DEADLINE_EXCEEDED - аналог 504, означает, что сервер не дождался ответа или вышло время запроса.
UNAVAILABLE - аналог 503, когда сервис временно недоступен.
При этом вот так выглядит стандартный синтаксис, как надо создавать ошибки:
status.Error(codes.Internal, "Internal server error!")
Также есть ситуации, когда клиенту мало знать код ошибки. В таких случаях используют status.WithDetails():
// Создаем статус с дополнительными деталями st := status.New(codes.InvalidArgument, "validation failed") // Добавляем структурированные детали st, _ = st.WithDetails(&errdetails.BadRequest{ FieldViolations: []*errdetails.BadRequest_FieldViolation{ { Field: "email", Description: "must be a valid email address", }, { Field: "password", Description: "must be at least 8 characters", }, }, }) return nil, st.Err()
При разработке важно понимать, что ошибки - это важная часть работы API. Важно всегда стараться передавать конкретную ошибку и отделять ошибки сервера от ошибок бизнес логики. При попытке вернуть вместо корректной ошибки nil, клиент просто потеряет важную информацию и может совершить неверную операцию.
На стороне клиента таким же образом генерируется Go-код из .proto. Клиент будет вызывать методы gRPC-сервера, для этого необходимо реализовать подключение к gRPC-серверу. Пример подключения в блоке кода ниже:
//Создаю подключение к gRPC-серверу: authServerAddr := os.Getenv("AUTH_SERVER_ADDR") if authServerAddr == "" { log.Printf("Ошибка получения адреса сервера аутентификации из переменных!") authServerAddr = "localhost:50051" } // Создаение клиента // ВАЖНО ЗАПОМНИТЬ, ЧТО ЭТО ПРИМЕР! В production-коде никогда не используйте insecure! // Всегда нужно настраивать TLS/mTLS! conn, err := grpc.NewClient(authServerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("Ошибка создания gRPC-клиента: %v", err) } defer conn.Close() client := pb.NewAuthClient(conn)
Важно понимать, что это лишь пример кода, реализации могут отличаться от разных версий и способов написания кода. В конкретном примере происходит подключение к порту 50051, который был указан при создании gRPC-сервера. Далее хотелось бы продемонстрировать пример вызова одного из методов, реализованных в gRPC-сервере. Будет рассматриваться вызов метода проверки аутентификации (CheckAuth):
func AuthMiddleware(next http.Handler, client CheckAuthInt) http.Handler{ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { cookie, err := req.Cookie("session") if err != nil { if err != http.ErrNoCookie { log.Printf("Error parse cookie: %v", err) } response.SendResponseError(w, http.StatusUnauthorized, "Unauthorized!") return } // Делаем запрос на сервер аутентификации для проверки токена: resp, err := client.CheckAuth(req.Context(), &pb.CheckAuthRequest{Token: cookie.Value}) if err != nil{ if status.Code(err) == codes.Unauthenticated{ response.SendResponseError(w, http.StatusUnauthorized, "Unauthorized!") return } response.SendResponseError(w, http.StatusInternalServerError, "Internal server error!") return } if !resp.IsAuth { response.SendResponseError(w, http.StatusUnauthorized, "Unauthorized!") return } next.ServeHTTP(w, req) }) }
Основной строкой здесь является resp, err := client.CheckAuth(req.Context(), &pb.CheckAuthRequest{Token: cookie.Value}), так как здесь происходит вызов самого метода. В аргументы передаются те значения, которые были указаны в .proto:
message CheckAuthRequest { string token = 1; }
Таким образом подобные вызовы можно встраивать в обработчики и делать операции так, как будто бы метод находится в том же сервисе, только в другом пакете.
В современных FAANG использование gRPC стало уже стандартом. С наибольшей долей вероятности любому разработчику, который стремиться разрабатывать высоконагруженные сервисы необходимо понимать gRPC и всё то, что с ним связанно, уметь работать с .proto-файлами и понимать взаимодействие сервисов по средством gRPC.
Полный код работы с gRPC вы можете посмотреть в моём архивном репозитории github, там же есть документация по запуску проекта в Docker и проверки работоспособности. Оба микросервиса располагаются в одной директории для удобства.
Спасибо за внимание! Если вы обнаружили ошибки или неточности в статье, вы можете указать об этом в комментариях!