본문으로 건너뛰기
한국어 번역 진행 중한국어 문서를 완성하는 동안 일부 가이드와 API 설명은 아직 영어로 표시될 수 있습니다.

HMAC-SHA256 서명

Ruby Team API에 대한 모든 요청은 HMAC-SHA256 서명을 사용하여 인증해야 합니다. 이 페이지에서는 각 요청을 구성하고 서명하는 방법을 설명합니다.

노트

이 서명 알고리즘은 Team API 요청에만 적용됩니다. 심리스 월렛 콜백을 구현하는 경우 콜백 서명 알고리즘이 다릅니다 — 자세한 내용은 콜백 인증을 참조하십시오.


필수 헤더

모든 요청에 다음 세 가지 헤더를 포함해야 합니다:

헤더
X-Team-KeyRuby에서 발급한 team_api_key
X-Team-Timestamp현재 유닉스 타임스탬프(초) (예: 1711500000)
X-Team-SignatureHMAC-SHA256 서명 (16진수 인코딩)

서명 구성

1단계 — 경로 문자열 구성

요청 경로를 사용합니다. 요청에 쿼리 스트링이 있는 경우 경로에 추가합니다:

path = "/api/brand/123"                        # 쿼리 스트링 없음
path = "/api/bet/list?page=1&size=20" # 쿼리 스트링 포함

2단계 — 서명 문자열 조립

정확히 다음 순서로 네 개의 필드를 구분자 없이 연결합니다:

signature_string = {timestamp}{HTTP_METHOD}{path}{raw_body}
필드상세
timestampX-Team-Timestamp에 전송하는 것과 동일한 유닉스 타임스탬프 값
HTTP_METHOD대문자 HTTP 메서드: GET, POST, PUT
path쿼리 스트링이 있는 경우 포함한 경로 (1단계에서)
raw_body네트워크를 통해 전송되는 것과 정확히 동일한 원시 요청 본문. GET 요청 또는 본문이 없는 요청의 경우 빈 문자열 "" 사용

3단계 — 서명 계산

signature = HMAC-SHA256(key=team_api_secret, message=signature_string)
  • 키: team_api_secret (UTF-8 인코딩 바이트)
  • 메시지: 서명 문자열 (UTF-8 인코딩 바이트)
  • 출력: 소문자 16진수 다이제스트

실제 예제

예제 1: JSON 본문이 있는 PUT 요청

요청:      PUT /api/brand/123
본문: {"status": 0}
타임스탬프: 1711500000

서명 문자열:

1711500000PUT/api/brand/123{"status": 0}
본문은 바이트 단위로 동일해야 합니다

서명 문자열에 포함되는 본문은 실제 전송하는 HTTP 요청 본문과 문자 단위로 동일해야 합니다. 이 예에서 {"status": 0}은 콜론 뒤에 공백이 있습니다. HTTP 클라이언트가 공백 없이 JSON을 직렬화하는 경우(즉, {"status":0}), 서명 문자열에서도 정확히 해당 형식을 사용하십시오. 서명을 위해 본문을 다시 포맷하거나 재직렬화하지 마십시오.

예제 2: 쿼리 스트링이 있는 GET 요청

요청:      GET /api/bet/list?page=1&size=20
타임스탬프: 1711500000

서명 문자열 (본문 없음 — 빈 문자열 사용):

1711500000GET/api/bet/list?page=1&size=20

타임스탬프 허용 범위

서버는 타임스탬프가 서버 시간으로부터 ±300초 이내인 요청을 허용합니다. 이 범위를 벗어나는 요청은 401 Unauthorized 오류로 거부됩니다. 시스템 시계가 동기화되어 있는지 확인하십시오 (보안 모범 사례 참조).


코드 예제

Python

import hashlib
import hmac
import time
import requests

def build_signature(
api_secret: str,
timestamp: int,
method: str,
path: str,
body: str = "",
) -> str:
"""Ruby Team API 요청을 위한 HMAC-SHA256 서명을 계산합니다."""
message = f"{timestamp}{method.upper()}{path}{body}"
return hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()


def make_request(api_key: str, api_secret: str, method: str, url: str, body: str = ""):
"""Ruby Team API에 인증된 요청을 전송합니다."""
timestamp = int(time.time())

# 전체 URL에서 경로(쿼리 스트링 포함) 파싱
from urllib.parse import urlparse
parsed = urlparse(url)
path = parsed.path
if parsed.query:
path = f"{path}?{parsed.query}"

signature = build_signature(api_secret, timestamp, method, path, body)

headers = {
"X-Team-Key": api_key,
"X-Team-Timestamp": str(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
}
return requests.request(method, url, headers=headers, data=body.encode("utf-8") if body else None)


# 예제: 브랜드 상태 업데이트
body = '{"status": 0}'
response = make_request(
api_key="your_team_api_key",
api_secret="your_team_api_secret",
method="PUT",
url="https://api.ruby.example.com/api/brand/123",
body=body,
)
print(response.json())

Node.js

const crypto = require("crypto");
const https = require("https");
const { URL } = require("url");

function buildSignature(apiSecret, timestamp, method, path, body = "") {
const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
return crypto
.createHmac("sha256", apiSecret)
.update(message, "utf8")
.digest("hex");
}

function makeRequest(apiKey, apiSecret, method, urlString, body = "") {
const timestamp = Math.floor(Date.now() / 1000);
const url = new URL(urlString);
const path = url.search ? `${url.pathname}${url.search}` : url.pathname;

const signature = buildSignature(apiSecret, timestamp, method, path, body);

const options = {
hostname: url.hostname,
path: `${url.pathname}${url.search}`,
method: method.toUpperCase(),
headers: {
"X-Team-Key": apiKey,
"X-Team-Timestamp": String(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
},
};

return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve(JSON.parse(data)));
});
req.on("error", reject);
if (body) req.write(body);
req.end();
});
}

// 예제: 베팅 목록 조회
makeRequest(
"your_team_api_key",
"your_team_api_secret",
"GET",
"https://api.ruby.example.com/api/bet/list?page=1&size=20"
).then(console.log);

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;

public class RubyApiClient {

private final String apiKey;
private final String apiSecret;
private final HttpClient httpClient = HttpClient.newHttpClient();

public RubyApiClient(String apiKey, String apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}

public String buildSignature(long timestamp, String method, String path, String body)
throws NoSuchAlgorithmException, InvalidKeyException {
String message = timestamp + method.toUpperCase() + path + (body != null ? body : "");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(raw);
}

public HttpResponse<String> makeRequest(String method, String urlString, String body)
throws Exception {
long timestamp = Instant.now().getEpochSecond();
URI uri = URI.create(urlString);
String path = uri.getRawQuery() != null
? uri.getRawPath() + "?" + uri.getRawQuery()
: uri.getRawPath();

String signature = buildSignature(timestamp, method, path, body);

HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(uri)
.header("X-Team-Key", apiKey)
.header("X-Team-Timestamp", String.valueOf(timestamp))
.header("X-Team-Signature", signature)
.header("Content-Type", "application/json");

if (body != null && !body.isEmpty()) {
builder.method(method.toUpperCase(),
HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else {
builder.method(method.toUpperCase(), HttpRequest.BodyPublishers.noBody());
}

return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
}

// 사용 예제
public static void main(String[] args) throws Exception {
RubyApiClient client = new RubyApiClient("your_team_api_key", "your_team_api_secret");

// 본문이 있는 PUT 요청
String body = "{\"status\": 0}";
HttpResponse<String> response = client.makeRequest(
"PUT",
"https://api.ruby.example.com/api/brand/123",
body
);
System.out.println(response.body());
}
}

자주 하는 실수

실수결과해결 방법
경로에 쿼리 스트링 누락서명 불일치 (401)쿼리 스트링이 있을 때 항상 ?query_string을 추가하십시오
서명 전 JSON 본문 재직렬화서명 불일치 (401)요청에서 전송하는 것과 정확히 동일한 원시 본문 바이트를 사용하십시오
만료된 타임스탬프 (300초 이상 차이)타임스탬프 범위 초과 (401)NTP로 시스템 시계를 동기화하십시오
잘못된 메서드 대소문자로 서명서명 불일치 (401)항상 대문자 메서드를 사용하십시오 (GET, PUT 등)
GET 요청에 본문 전송예기치 않은 동작본문이 없는 요청에는 빈 문자열 ""을 사용하십시오