Grpc на go: как создать микросервис аутентификации с нуля

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 остаётся востребованным выбором для производительных распределённых приложений.

Прокрутить вверх