Микросервис авторизации на Go с JWT: от нуля до продакшена за 30 минут
Сколько раз вы писали авторизацию с нуля для нового пет-проекта? Копировали старый код, собирали либы, наскоро делали /login и /refresh? А потом думали о безопасности, структуре, тестах... Давайте один раз сделаем это правильно, но минимально. Сегодня мы соберем сервис, который станет вашим надежным go-to решением для будущих проектов.
Коротко о том, что будем строить: REST API с 3 эндпоинтами (/register, /login, /refresh)

Ключевые принципы: Разделение слоев (handlers, service, storage), чистота зависимостей, безопасность по умолчанию.
Начнем снизу. Для начала необходимо добавить необходимые минимальные модели.
//auth-service/internal/domain/models
type RefreshToken struct {
ID uuid.UUID `db:"id"`
Token string `db:"token"`
ExpiresAt time.Time `db:"expires_at"`
IsRevoked bool `db:"is_revoked"`
UserID uuid.UUID `db:"user_id"`
CreatedAt time.Time `db:"created_at"`
}
type User struct {
ID uuid.UUID `db:"id"`
Email string `db:"email"`
PasswordHash string `db:"password_hash"`
CreatedAt time.Time `db:"created_at"`
}Далее необходимо реализовать два репозитория для каждой сущности. Разберем на примере usecase Create()
// auth-service/internal/repository/postgres
type UserRepository struct {
db *pgxpool.Pool
}
func NewUserRepository(db *pgxpool.Pool) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *model.User) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.db.Exec(ctx,
"INSERT INTO users (id, email, password_hash, created_at) VALUES ($1, $2, $3, $4)",
user.ID, user.Email, user.PasswordHash, user.CreatedAt,
)
return err
}type RefreshTokenRepository struct {
db *pgxpool.Pool
}
func NewRefreshTokenRepository(db *pgxpool.Pool) *RefreshTokenRepository {
return &RefreshTokenRepository{db: db}
}
func (r *RefreshTokenRepository) Create(token *model.RefreshToken) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := r.db.Exec(ctx,
`INSERT INTO refresh_tokens (id, token, expires_at, is_revoked, user_id, created_at)
VALUES ($1, $2, $3, $4, $5, $6)`,
token.ID, token.Token, token.ExpiresAt, token.IsRevoked, token.UserID, token.CreatedAt,
)
return err
}Добавим необходимые сервисы для хеширования пароля и создания токена:
//auth-service/internal/infrastructure/jwt
type JWTService struct {
secret []byte
accessTokenTTL time.Duration
}
type AccessClaims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func NewJWTService(secret string, accessTTL time.Duration) *JWTService {
return &JWTService{
secret: []byte(secret),
accessTokenTTL: accessTTL,
}
}
func (s *JWTService) Generate(userID, email string) (*pkg_dto.TokenPair, error) {
now := time.Now()
claims := AccessClaims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(s.accessTokenTTL)),
IssuedAt: jwt.NewNumericDate(now),
},
}
accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
if err != nil {
return nil, err
}
return &pkg_dto.TokenPair{
AccessToken: accessToken,
RefreshToken: uuid.NewString(),
ExpiresAt: claims.ExpiresAt.Time,
}, nil
}//auth-service/internal/pkg/pkg_dto - dto для связи вспомогательных сервисов между собой
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
}А также:
//auth-service/internal/infrastructure/security
type BcryptHasher struct {
cost int
}
func NewBcryptHasher(cost int) *BcryptHasher {
return &BcryptHasher{cost: cost}
}
func (h *BcryptHasher) Hash(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword(
[]byte(password),
h.cost,
)
return string(bytes), err
}Для поддержания чистой архитектуры необходимо добавить прослойку сервиса, которая будет связывать все "запчасти" между собой.
//auth-service/internal/service
type PasswordHasher interface {
Hash(password string) (string, error)
}
type TokenService interface {
Generate(userID, email string) (*pkg_dto.TokenPair, error)
}
type RefreshTokenRepository interface {
Create(token *model.RefreshToken) error
}
type UserRepository interface {
Create(user *model.User) error
}
func (s *AuthService) Register(email, password string) (*dto.TokenResponse, error) {
hash, err := s.hasher.Hash(password)
if err != nil {
return nil, err
}
user := &model.User{
ID: uuid.New(),
Email: email,
PasswordHash: hash,
CreatedAt: time.Now().UTC(),
}
if err := s.usersRepo.Create(user); err != nil {
return nil, err
}
tokens, err := s.jwt.Generate(
user.ID.String(),
user.Email,
)
if err != nil {
return nil, err
}
rt := &model.RefreshToken{
ID: uuid.New(),
UserID: user.ID,
Token: tokens.RefreshToken,
ExpiresAt: tokens.ExpiresAt,
CreatedAt: time.Now().UTC(),
IsRevoked: false,
}
if err := s.tokensRepo.Create(rt); err != nil {
return nil, err
}
return &dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
ExpiresAt: tokens.ExpiresAt,
}, nil
}DTO для связи ручки и сервиса:
//auth-service/internal/service/dto
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
}
type RegisterRequest struct {
Email string `json:"email" example:"test@mail.com"`
Password string `json:"password" example:"qwerty123"`
}И последний этап - написание ручки для нашего usecase.
type AuthService interface {
Register(email, password string) (*dto.TokenResponse, error)
}
type AuthHandler struct {
authService AuthService
}
func NewAuthHandler(s AuthService) *AuthHandler {
return &AuthHandler{authService: s}
}
// RegisterHandler godoc
// @Summary Register a new user
// @Description Create a new user with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "User info"
// @Success 201 {object} dto.TokenResponse
// @Failure 400 {string} string "invalid body"
// @Router /auth/register [post]
func (h *AuthHandler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
var req dto.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
token, err := h.authService.Register(req.Email, req.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(token)
}Содержимое main.go:
func main() {
ctx := context.Background()
db, err := pgxpool.New(ctx, "postgres://myuser:mypassword@localhost:5432/mydatabase?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Репозитории
userRepo := postgres.NewUserRepository(db)
tokenRepo := postgres.NewRefreshTokenRepository(db)
hasher := security.NewBcryptHasher(bcrypt.DefaultCost)
jwtSvc := jwt.NewJWTService(
"super-secret-key", // secret
15*time.Minute, // access TTL
)
// Сервис
authSvc := service.NewAuthService(userRepo, tokenRepo, hasher, jwtSvc)
// Хендлер
authHandler := handler.NewAuthHandler(authSvc)
mux := http.NewServeMux()
mux.HandleFunc("/auth/register", authHandler.RegisterHandler)
mux.Handle("/swagger/", httpSwagger.WrapHandler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}