Система управління ключами API криптобіржи
API-ключі — це програмний аналог логіну/пароля. Трейдер створює ключ з певними правами, передає його боту або стороннему сервісу, і той торгує від його імені. Правильна система API-ключів означає гнучкі дозволи, безпечне зберігання та детальний журнал аудиту.
Модель даних API-ключів
type APIKey struct {
ID string // публічний ключ (наприклад: "ak_prod_a1b2c3d4...")
Secret string // хеш секрету (НІКОЛИ не зберігати простий текст)
UserID int64
Label string // "Trading Bot", "Portfolio Tracker"
// Дозволи
Permissions APIPermissions
// Обмеження
IPWhitelist []string // якщо порожній — будь-яка IP
ExpiresAt *time.Time
// Статус
IsActive bool
LastUsedAt *time.Time
CreatedAt time.Time
}
type APIPermissions struct {
// Торгівля
SpotTrade bool
MarginTrade bool
FuturesTrade bool
// Рахунок
ReadAccount bool // баланси, історія
Withdraw bool // ПОПЕРЕДЖЕННЯ: високий ризик
// Ринкові дані
ReadMarketData bool // завжди ввімкнено для безплатного доступу
}
Дозвіл на виведення — найнебезпечніший. Рекомендація: окреме підтвердження при ввімкненні, окремий whitelist адрес для цього ключа, сповіщення електронною поштою.
Генерація та зберігання ключів
import (
"crypto/rand"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
func GenerateAPIKey() (publicKey, secretKey string, err error) {
// Публічний ключ: 32 байти, hex-кодований
pubBytes := make([]byte, 16)
if _, err = rand.Read(pubBytes); err != nil {
return
}
publicKey = "ak_" + hex.EncodeToString(pubBytes)
// Секрет: 32 байти, hex-кодований
secBytes := make([]byte, 32)
if _, err = rand.Read(secBytes); err != nil {
return
}
secretKey = hex.EncodeToString(secBytes)
return
}
func HashSecret(secret string) (string, error) {
// bcrypt для зберігання — повільний хеш, стійкий до brute force
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
return string(hash), err
}
func VerifySecret(secret, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(secret)) == nil
}
Критично: секретний ключ показується користувачу ОДИН РАЗ при створенні. В базі зберігається лише bcrypt хеш. Якщо користувач втратив секрет — потрібно створити новий ключ.
Middleware аутентифікації
func APIKeyAuthMiddleware(db *DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKeyID := r.Header.Get("X-API-Key")
signature := r.Header.Get("X-Signature")
timestamp := r.Header.Get("X-Timestamp")
if apiKeyID == "" || signature == "" {
writeError(w, 401, "Missing authentication headers")
return
}
// 1. Знаходимо ключ по публічному ID
apiKey, err := db.GetAPIKey(apiKeyID)
if err != nil || !apiKey.IsActive {
writeError(w, 401, "Invalid API key")
return
}
// 2. Перевірка часової мітки (анти-replay, ±5 сек)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if abs(time.Now().UnixMilli()-ts) > 5000 {
writeError(w, 401, "Timestamp out of range")
return
}
// 3. Верифікація підпису (HMAC-SHA256)
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(body))
message := r.Method + r.URL.RequestURI() + timestamp + string(body)
// Використовуємо секрет з кеша (вилучення хешу неможливе — потрібен окремий кеш)
if !verifyHMAC(message, apiKey.SecretForVerification, signature) {
writeError(w, 401, "Invalid signature")
return
}
// 4. IP whitelist
if len(apiKey.IPWhitelist) > 0 {
clientIP := getClientIP(r)
if !contains(apiKey.IPWhitelist, clientIP) {
writeError(w, 403, "IP not whitelisted")
return
}
}
// 5. Оновлюємо last_used_at асинхронно
go db.UpdateLastUsed(apiKey.ID)
// Передаємо контекст
ctx := context.WithValue(r.Context(), "api_key", apiKey)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Важливе зауваження: HMAC верифікація вимагає знання секрету, але ми зберігаємо лише bcrypt хеш. Рішення: при створенні ключа зберігаємо секрет у зашифрованому вигляді (AES-256 з ключем із HSM) лише для HMAC верифікації, а не для повторного показу користувачу.
Перевірки дозволів
func RequirePermission(perm string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Context().Value("api_key").(APIKey)
hasPermission := false
switch perm {
case "spot_trade":
hasPermission = apiKey.Permissions.SpotTrade
case "withdraw":
hasPermission = apiKey.Permissions.Withdraw
case "read_account":
hasPermission = apiKey.Permissions.ReadAccount
}
if !hasPermission {
writeError(w, 403, fmt.Sprintf("Permission denied: %s required", perm))
return
}
next.ServeHTTP(w, r)
})
}
}
// Використання:
router.POST("/api/v1/orders",
APIKeyAuthMiddleware(db),
RequirePermission("spot_trade"),
handler.PlaceOrder)
router.POST("/api/v1/withdrawals",
APIKeyAuthMiddleware(db),
RequirePermission("withdraw"),
handler.CreateWithdrawal)
UI управління ключами
// Сторінка управління API ключами
function APIKeysManager() {
const [keys, setKeys] = useState<APIKey[]>([]);
const [showCreateModal, setShowCreateModal] = useState(false);
return (
<div>
<Button onClick={() => setShowCreateModal(true)}>Create New API Key</Button>
<table>
{keys.map(key => (
<tr key={key.id}>
<td>{key.label}</td>
<td><code>{key.id}</code></td>
<td><PermissionBadges permissions={key.permissions} /></td>
<td>{key.ipWhitelist.length > 0 ? key.ipWhitelist.join(', ') : 'All IPs'}</td>
<td>{key.lastUsedAt ? formatRelative(key.lastUsedAt) : 'Never'}</td>
<td>
<ToggleButton active={key.isActive} onToggle={() => toggleKey(key.id)} />
<DeleteButton onClick={() => deleteKey(key.id)} />
</td>
</tr>
))}
</table>
{showCreateModal && <CreateAPIKeyModal onCreated={handleKeyCreated} />}
</div>
);
}
// Після створення — показуємо секрет ОДИН РАЗ
function SecretRevealModal({ secret }: { secret: string }) {
const [copied, setCopied] = useState(false);
return (
<Modal>
<Alert type="warning">
Скопіюйте секретний ключ зараз. Він більше не буде показаний.
</Alert>
<CodeBlock value={secret} />
<CopyButton value={secret} onCopy={() => setCopied(true)} />
<Button disabled={!copied} onClick={closeModal}>
Я скопіював ключ
</Button>
</Modal>
);
}
Журнал аудиту
Кожен запит API логується для безпеки:
CREATE TABLE api_access_log (
id BIGSERIAL PRIMARY KEY,
api_key_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
method VARCHAR(10) NOT NULL,
path VARCHAR(255) NOT NULL,
ip_address INET NOT NULL,
status_code SMALLINT NOT NULL,
latency_ms INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
-- Зберігаємо 90 днів, старі видаляємо
CREATE INDEX idx_api_log_key_time ON api_access_log(api_key_id, created_at DESC);
Розробка повної системи управління API-ключами з дозволами, IP whitelist, журналом аудиту та UI: 3–4 тижні.







