114 lines
3.9 KiB
Go
114 lines
3.9 KiB
Go
// app/auth/cache.go
|
|
// Кэш успешной валидации JWT-токенов (в памяти процесса).
|
|
// Мотивация: Nubes-шлюз нестабильно отвечает на запросы с IP платформы
|
|
// (то 200, то 401/403), из-за чего повторная валидация на каждый запрос
|
|
// консоли роняла UI. Кэш: одна успешная проверка → TTL без обращений к шлюзу.
|
|
// Безопасность: отозванный токен перестаёт работать максимум через TTL;
|
|
// явный 401/403 сбрасывает запись немедленно.
|
|
// Created: 2026-08-14
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// cacheTTL — срок жизни записи успешной валидации.
|
|
const cacheTTL = 5 * time.Minute
|
|
|
|
// cacheGraceTTL — продление при сетевой ошибке шлюза для ранее валидного токена.
|
|
const cacheGraceTTL = 2 * time.Minute
|
|
|
|
// cacheCleanupInterval — период фоновой очистки протухших записей.
|
|
const cacheCleanupInterval = 5 * time.Minute
|
|
|
|
type tokenCacheEntry struct {
|
|
validUntil time.Time
|
|
}
|
|
|
|
var (
|
|
tokenCacheMu sync.RWMutex
|
|
tokenCache = map[string]tokenCacheEntry{}
|
|
)
|
|
|
|
// tokenCacheKey — компактный ключ кэша: hex(SHA256(token)).
|
|
func tokenCacheKey(token string) string {
|
|
h := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
// PingNubesAPICached — валидация токена через шлюз с кэшем.
|
|
// HIT (запись валидна) → nil без обращения к шлюзу.
|
|
// MISS → свежая проверка: успех → запись с TTL; 401/403 → удаление записи и ошибка.
|
|
// Сетевая ошибка: если запись была (пусть и протухшая) → grace-продление и nil,
|
|
// иначе — ошибка (шлюз недоступен, кэша нет — справедливый отказ).
|
|
func PingNubesAPICached(ctx context.Context, endpoint, token string) error {
|
|
key := tokenCacheKey(token)
|
|
now := time.Now()
|
|
|
|
tokenCacheMu.RLock()
|
|
entry, ok := tokenCache[key]
|
|
tokenCacheMu.RUnlock()
|
|
if ok && now.Before(entry.validUntil) {
|
|
return nil // cache HIT
|
|
}
|
|
|
|
err := PingNubesAPI(ctx, endpoint, token)
|
|
|
|
if errors.Is(err, ErrTokenRejected) {
|
|
tokenCacheMu.Lock()
|
|
delete(tokenCache, key)
|
|
tokenCacheMu.Unlock()
|
|
return err
|
|
}
|
|
if err != nil {
|
|
// Сетевая ошибка шлюза: если токен ранее был валиден — даём grace.
|
|
if ok {
|
|
tokenCacheMu.Lock()
|
|
tokenCache[key] = tokenCacheEntry{validUntil: time.Now().Add(cacheGraceTTL)}
|
|
tokenCacheMu.Unlock()
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
tokenCacheMu.Lock()
|
|
tokenCache[key] = tokenCacheEntry{validUntil: time.Now().Add(cacheTTL)}
|
|
tokenCacheMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// WarmTokenCache — записывает токен в кэш как валидный без вызова шлюза.
|
|
// Используется после успешной свежей проверки при логине (jwtAuth).
|
|
func WarmTokenCache(token string) {
|
|
tokenCacheMu.Lock()
|
|
tokenCache[tokenCacheKey(token)] = tokenCacheEntry{validUntil: time.Now().Add(cacheTTL)}
|
|
tokenCacheMu.Unlock()
|
|
}
|
|
|
|
var tokenCacheCleanupOnce sync.Once
|
|
|
|
// StartTokenCacheCleanup — фоновая очистка протухших записей. Идемпотентна (sync.Once).
|
|
func StartTokenCacheCleanup() {
|
|
tokenCacheCleanupOnce.Do(func() {
|
|
go func() {
|
|
ticker := time.NewTicker(cacheCleanupInterval)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
now := time.Now()
|
|
tokenCacheMu.Lock()
|
|
for k, e := range tokenCache {
|
|
if now.After(e.validUntil) {
|
|
delete(tokenCache, k)
|
|
}
|
|
}
|
|
tokenCacheMu.Unlock()
|
|
}
|
|
}()
|
|
})
|
|
}
|