내 앱에 통합하기

요청 서명

#필요한 경우

자체 백엔드가 없는 앱은 키를 앱 안에 넣어 배포해야 하므로, 트래픽을 캡처하면 누구나 읽을 수 있습니다. 해당 키에 요청 서명을 켜세요(콘솔 → API 키 → 편집 → 요청 서명). 그러면 모든 요청에 별도의 서명 시크릿으로 만든 서명이 함께 있어야 하므로, sk- 키만으로는 쓸모가 없고 캡처한 요청을 다시 보낼 수도 없습니다.

#세 가지 헤더

Authorization은 그대로 보내고 헤더 세 개를 추가하세요. 서명할 텍스트는 타임스탬프, nonce, HTTP 메서드, 경로(쿼리 문자열 제외)를 줄바꿈으로 연결한 것이며, HMAC 키는 콘솔에 표시된 sig_… 시크릿을 그대로 사용합니다.

headers
X-Timestamp: 1759312345        # unix seconds, within 5 minutes of server time
X-Nonce:     9f2c4e7a1b3d5f60  # 16-128 chars of A-Z a-z 0-9 - _, new for every request
X-Signature: hex(HMAC-SHA256(secret, "1759312345\n9f2c4e7a1b3d5f60\nPOST\n/v1/images/tasks"))

#요청에 서명하기

sign.dart
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

const apiKey = 'sk-YOUR_KEY';
const signingSecret = 'sig_YOUR_SIGNING_SECRET';
int clockOffset = 0; // server time - device time, from X-Server-Timestamp

Map<String, String> signedHeaders(String method, String path) {
  final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000 + clockOffset).toString();
  final random = Random.secure();
  final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();
  final signature = Hmac(sha256, utf8.encode(signingSecret))
      .convert(utf8.encode('$ts\n$nonce\n$method\n$path'))
      .toString();
  return {
    'Authorization': 'Bearer $apiKey',
    'X-Timestamp': ts,
    'X-Nonce': nonce,
    'X-Signature': signature,
  };
}

// headers: signedHeaders('POST', '/v1/images/tasks')
// GET /v1/images/tasks/{id} is signed with its own path, e.g. '/v1/images/tasks/task_123'
sign.mjs
import crypto from "node:crypto";

const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const signature = crypto
  .createHmac("sha256", process.env.SIGNING_SECRET)
  .update(`${ts}\n${nonce}\nPOST\n/v1/images/tasks`)
  .digest("hex");

await fetch("https://www.aisplice.store/v1/images/tasks", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk-YOUR_KEY",
    "Content-Type": "application/json",
    "X-Timestamp": ts,
    "X-Nonce": nonce,
    "X-Signature": signature,
  },
  body: JSON.stringify(payload),
});

#오류와 규칙

  • 거부된 요청은 401과 함께 error.code로 signature_required, signature_expired, signature_invalid 또는 signature_replayed를 반환합니다.
  • signature_expired는 X-Server-Timestamp도 함께 반환합니다. 시계가 틀린 휴대폰은 그 차이를 저장해 모든 타임스탬프에 더하고, 새 nonce로 한 번 재시도하세요.
  • 재시도할 때도 새 nonce와 새 서명이 필요합니다. nonce는 10분 동안 기억됩니다.
  • 콘솔에서 시크릿을 교체하면 이전 시크릿은 30초 안에 무효화됩니다. 서명을 껐다가 다시 켜도 시크릿은 그대로입니다.
  • 서명은 트래픽 캡처는 막아 주지만, 앱을 뜯어보는 사람까지 막지는 못합니다. 빌드를 난독화하고(Flutter: --obfuscate) 키에 한도 상한을 두세요.

아직 해결되지 않았나요?

요청 ID나 오류 메시지를 보내 주시면 확인해 드리겠습니다.