gRPC на Go: создаём микросервис аутентификации с нуля
gRPC давно стал одним из популярных инструментов для обмена данными между внутренними сервисами. В экосистеме Go он особенно удобен: язык хорошо поддерживает параллельную обработку запросов, имеет производительный стандартный сетевой стек и развитую библиотеку для работы с protobuf. В результате разработка gRPC сервисов на Go позволяет строить быстрые и строго типизированные компоненты для микросервисной архитектуры.
В этой статье разберём, как устроен простой сервис аутентификации. Он будет принимать учётные данные пользователя, выполнять проверку и возвращать результат через gRPC-интерфейс. Такой пример помогает понять базовые принципы: описание API в Protobuf, генерацию Go-кода, запуск сервера, обработку ошибок и подключение клиента.
gRPC - это фреймворк удалённого вызова процедур, изначально созданный Google. Один сервис может вызывать методы другого почти так же, как локальные функции, хотя фактически запрос проходит по сети. В отличие от REST-подхода с JSON, gRPC обычно использует бинарную сериализацию Protocol Buffers и транспорт HTTP/2.
HTTP/2 обеспечивает мультиплексирование запросов в одном соединении, потоковую передачу данных и более эффективное использование сети. Бинарный формат уменьшает размер сообщений и снижает затраты на их обработку. Поэтому Go микросервисы с gRPC часто выбирают для внутренних взаимодействий, где важны скорость, предсказуемость и строгий контракт.
Protobuf как контракт API
Основой gRPC является файл с расширением `.proto`. В нём описываются сообщения, методы и сервисы. Это не обычная документация: схема используется компилятором `protoc` для генерации исходного кода на Go и других языках.
Пример контракта для сервиса авторизации может выглядеть так:
```proto
syntax = "proto3";
package auth;
option go_package = "example/authpb";
service Auth {
rpc Login(LoginRequest) returns (LoginResponse);
}
message LoginRequest {
string username = 1;
string password = 2;
}
message LoginResponse {
bool success = 1;
string token = 2;
}
```
Здесь объявлен сервис `Auth` с методом `Login`. У каждого RPC-метода явно указаны тип входного и выходного сообщения. Благодаря этому клиент и сервер используют одинаковое представление данных, а изменения API можно контролировать через версионирование схемы.
Protocol Buffers компактнее JSON, поскольку значения передаются в бинарном виде, а не как текстовые ключи и строки. При этом protobuf не ограничивается сетевым обменом: его применяют для хранения структурированных данных и построения межъязыковых протоколов.
В актуальном синтаксисе proto3 доступны карты, вложенные структуры, специальные типы для времени и динамических значений, а также `optional`. Последний особенно важен для различения "поле отсутствует" и "поле содержит значение по умолчанию". Например, ноль для числового поля или пустая строка раньше могли не позволить понять, передавалось ли значение вообще.
Для сборки понадобятся `protoc` и плагины Go:
```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```
Каталог с установленными бинарными файлами должен находиться в `PATH`. В Bash это можно настроить так:
```bash
export PATH="$PATH:$(go env GOPATH)/bin"
```
Генерация выполняется командой:
```bash
protoc
--go_out=.
--go_opt=paths=source_relative
--go-grpc_out=.
--go-grpc_opt=paths=source_relative
auth.proto
```
После этого появятся два файла. `auth.pb.go` содержит структуры сообщений и код сериализации. В `auth_grpc.pb.go` находятся интерфейс сервера, клиентская обёртка и описание RPC-методов.
Редактировать сгенерированные файлы вручную не следует. Любое изменение нужно вносить в `.proto`, а затем повторно запускать генерацию. Иначе схема и программный код могут разойтись, что приведёт к ошибкам компиляции или несовместимости клиентов.
Реализация сервера
В классической модели gRPC клиентом является не браузер, а другой сервис. Например, внешний веб-сервер может принимать HTTP-запросы от пользовательского интерфейса и обращаться к внутреннему сервису аутентификации по gRPC. Такой подход часто называют BFF: Backend for Frontend выступает адаптером между публичным HTTP API и внутренними сервисами.
Минимальная реализация сервера должна содержать структуру, реализующую сгенерированный интерфейс:
```go
type AuthServer struct {
authpb.UnimplementedAuthServer
}
func (s *AuthServer) Login(
ctx context.Context,
req *authpb.LoginRequest,
) (*authpb.LoginResponse, error) {
if req.GetUsername() == "admin" && req.GetPassword() == "secret" {
return &authpb.LoginResponse{
Success: true,
Token: "demo-token",
}, nil
}
return nil, status.Error(
codes.Unauthenticated,
"invalid credentials",
)
}
```
Встраивание `UnimplementedAuthServer` рекомендуется сохранять: оно помогает поддерживать совместимость при добавлении новых методов в контракт.
Запуск gRPC-сервера выглядит следующим образом:
```go
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
grpcServer := grpc.NewServer()
authpb.RegisterAuthServer(grpcServer, &AuthServer{})
log.Println("auth service listens on :50051")
if err := grpcServer.Serve(listener); err != nil {
log.Fatal(err)
}
}
```
В реальном проекте проверка логина и пароля, разумеется, должна выполняться через базу данных или отдельный слой бизнес-логики. Пароли нельзя хранить в открытом виде, а токен следует формировать с использованием безопасного механизма, например JWT с корректным сроком действия и подписью.
Ошибки и статусы gRPC
Обычный `errors.New` подходит для внутренних проблем, но клиенту gRPC важно передавать стандартизированный код. Для этого используется пакет `status`:
```go
return nil, status.Error(
codes.Unauthenticated,
"invalid credentials",
)
```
Помимо `Unauthenticated`, часто применяются:
- `InvalidArgument` - некорректные входные данные;
- `NotFound` - объект не найден;
- `AlreadyExists` - конфликт при создании;
- `PermissionDenied` - недостаточно прав;
- `Unavailable` - сервис временно недоступен;
- `Internal` - непредвиденная ошибка на стороне сервера.
Корректные коды позволяют клиенту отличать ошибку пользователя от сбоя инфраструктуры и выбирать подходящую стратегию: показать сообщение, повторить запрос или передать проблему выше.
Клиентская часть
Клиент создаёт соединение с сервером и использует сгенерированный объект:
```go
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := authpb.NewAuthClient(conn)
response, err := client.Login(
context.Background(),
&authpb.LoginRequest{
Username: "admin",
Password: "secret",
},
)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.GetSuccess(), response.GetToken())
```
Для локального тестирования допустимо использовать `insecure.NewCredentials()`, однако в production-среде соединение должно быть защищено TLS. Также полезно передавать контекст с тайм-аутом, чтобы зависший сервис не удерживал ресурсы бесконечно:
```go
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)
defer cancel()
```
Практический пример того, как устроен микросервис аутентификации на Go, помогает связать теорию protobuf и gRPC с полноценной клиент-серверной реализацией.
Что важно добавить в production
Простейший пример показывает только транспортный слой. В настоящем сервисе аутентификации потребуются middleware для логирования, трассировки и проверки метаданных. Токен обычно передаётся в заголовке `authorization`, а серверный interceptor проверяет его до вызова бизнес-метода.
Не стоит забывать и о защите от перебора паролей: нужны ограничения частоты запросов, временная блокировка подозрительных клиентов и аудит неудачных попыток входа. Секреты, ключи подписи и параметры подключения к базе должны храниться в менеджере секретов или переменных окружения, а не в исходном коде.
Для тестирования удобно использовать отдельные интеграционные сценарии: запускать сервер на тестовом порту, отправлять корректные и ошибочные запросы, проверять gRPC-коды и поведение при недоступности базы. Кроме того, protobuf-контракты стоит проверять на совместимость, чтобы новые версии клиентов не ломали старые сервисы.
В крупных системах аутентификация редко ограничивается одним методом входа. В дальнейшем можно добавить обновление токена, отзыв сессий, проверку ролей, двухфакторную авторизацию и интеграцию с внешним провайдером идентификации. При этом базовая архитектура останется прежней: контракт в `.proto`, сгенерированный код, серверная реализация и клиентский вызов.
Таким образом, аутентификация в микросервисах Go хорошо демонстрирует преимущества gRPC: строгую типизацию, компактный формат сообщений и удобную генерацию клиентского и серверного кода. Освоив этот пример, можно переходить к построению более сложных систем - от каталога и платежного сервиса до единой платформы, где взаимодействуют десятки внутренних компонентов. Именно поэтому gRPC на Go остаётся востребованным выбором для производительных распределённых приложений.


