Неделя 6: AuthService, gRPC, Postgres, Redis sessions
К этому моменту в системе шесть сервисов (Order, Inventory, Payment, Assembly, Notification-mini, Analytics) и три типа хранилищ (Postgres у нескольких сервисов, ClickHouse у Analytics, Kafka как шина событий). Все запросы пока анонимны, любой может создать заказ от любого user_uuid. Этой неделей чиним: добавляем седьмой сервис, AuthService, и сессии.
Цели недели
- Создать новый сервис AuthService с тем же layout что у предыдущих.
- Реализовать gRPC API: Register, Login, Whoami, GetUser.
- Хранить пользователей в Postgres, сессии в Redis.
- Хешировать пароли через bcrypt.
- Поднять Redis в docker-compose.
- Реализовать 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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.