Раздел 24 · Микросервисы (Go)

Неделя 6: AuthService, gRPC, Postgres, Redis sessions

middle-senior~55 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Неделя 6: AuthService, gRPC, Postgres, Redis sessions

К этому моменту в системе шесть сервисов (Order, Inventory, Payment, Assembly, Notification-mini, Analytics) и три типа хранилищ (Postgres у нескольких сервисов, ClickHouse у Analytics, Kafka как шина событий). Все запросы пока анонимны, любой может создать заказ от любого user_uuid. Этой неделей чиним: добавляем седьмой сервис, AuthService, и сессии.

Цели недели

  1. Создать новый сервис AuthService с тем же layout что у предыдущих.
  2. Реализовать gRPC API: Register, Login, Whoami, GetUser.
  3. Хранить пользователей в Postgres, сессии в Redis.
  4. Хешировать пароли через bcrypt.
  5. Поднять Redis в docker-compose.
  6. Реализовать middleware для gRPC и HTTP, проверяющий сессию.

Архитектура AuthService

auth/
├── cmd/main.go
├── go.mod
├── migrations/
│   └── 20260601_init.sql
└── internal/
    ├── api/auth/v1/                   # реализация gRPC server интерфейса
    │   ├── api.go
    │   ├── register.go
    │   ├── login.go
    │   ├── whoami.go
    │   └── get_user.go
    ├── app/
    │   ├── app.go
    │   └── di.go
    ├── config/
    ├── model/
    │   ├── user.go
    │   ├── session.go
    │   └── errors.go
    ├── repository/
    │   ├── user/                       # Postgres
    │   └── session/                    # Redis
    └── service/
        └── auth/
            ├── service.go
            ├── register.go
            ├── login.go
            ├── whoami.go
            └── get_user.go

Контракт gRPC

shared/proto/auth/v1/auth.proto:

syntax = "proto3";

package auth.v1;

option go_package = "shared/pkg/proto/auth/v1;auth_v1";

service AuthService {
    rpc Register(RegisterRequest) returns (RegisterResponse);
    rpc Login(LoginRequest) returns (LoginResponse);
    rpc Whoami(WhoamiRequest) returns (WhoamiResponse);
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message RegisterRequest {
    string email = 1;
    string password = 2;
    repeated NotificationMethod notification_methods = 3;
}
message RegisterResponse {
    string user_uuid = 1;
}

message LoginRequest {
    string email = 1;
    string password = 2;
}
message LoginResponse {
    string session_token = 1;
    int64  expires_at = 2; // unix seconds
}

message WhoamiRequest {
    string session_token = 1;
}
message WhoamiResponse {
    string user_uuid = 1;
    string email = 2;
    int64  expires_at = 3;
}

message GetUserRequest { string user_uuid = 1; }
message GetUserResponse {
    string user_uuid = 1;
    string email = 2;
    repeated NotificationMethod notification_methods = 3;
}

message NotificationMethod {
    string kind = 1;   // "telegram" | "email" | "sms"
    string target = 2; // chat_id, email address, phone number
}

Whoami нужен в первую очередь не клиенту, а другим сервисам (Envoy gateway, gRPC interceptor) для проверки валидности сессии.

Хранение пользователей в Postgres

-- migrations/20260601_init.sql

-- +goose Up
CREATE TABLE users (
    uuid                 UUID PRIMARY KEY,
    email                TEXT NOT NULL UNIQUE,
    password_hash        TEXT NOT NULL,
    notification_methods JSONB NOT NULL DEFAULT '[]'::jsonb,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX users_email_lower_idx ON users (LOWER(email));

-- +goose Down
DROP TABLE users;

UNIQUE на email плюс индекс на LOWER(email), чтобы регистр не мешал unique-проверке (User@Example.com и user@example.com, один человек).

notification_methods в JSONB, потому что у разных пользователей разный список методов и нет смысла нормализовать в отдельную таблицу.

Хранение сессий в Redis

Redis идеально подходит для сессий: TTL из коробки, быстрый lookup, кластеризация если потребуется.

Ключ: session:{token}. Значение: JSON {user_uuid, expires_at}. TTL 24 часа.

type Repository struct {
    client *redis.Client
}

const sessionTTL = 24 * time.Hour

func (r *Repository) Create(ctx context.Context, s model.Session) error {
    payload, err := json.Marshal(s)
    if err != nil { return err }
    return r.client.Set(ctx, "session:"+s.Token, payload, sessionTTL).Err()
}

func (r *Repository) Get(ctx context.Context, token string) (model.Session, error) {
    raw, err := r.client.Get(ctx, "session:"+token).Bytes()
    if err != nil {
        if errors.Is(err, redis.Nil) {
            return model.Session{}, model.ErrSessionNotFound
        }
        return model.Session{}, err
    }
    var s model.Session
    if err := json.Unmarshal(raw, &s); err != nil {
        return model.Session{}, err
    }
    return s, nil
}

func (r *Repository) Revoke(ctx context.Context, token string) error {
    return r.client.Del(ctx, "session:"+token).Err()
}

Token, 32 случайных байта в hex или base64url. Хранить в Redis сам токен (не его хеш) допустимо для in-memory storage с коротким TTL, но более параноидальный вариант, хранить sha256(token).

bcrypt для паролей

import "golang.org/x/crypto/bcrypt"

func hashPassword(plain string) (string, error) {
    h, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
    if err != nil { return "", err }
    return string(h), nil
}

func checkPassword(hashed, plain string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)) == nil
}

DefaultCost, на 2026 год это 10. Если нужны более строгие требования, ставь 12, 14 (но осторожно, на cost 14 на одной машине bcrypt считает ~1 секунду).

Альтернативы: argon2id (более современный, выше требования к памяти), scrypt. Для курса bcrypt достаточно. Никогда не используй MD5, SHA-1, plain SHA-256 без соли.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.