HMAC-SHA256 서명
Ruby Team API에 대한 모든 요청은 HMAC-SHA256 서명을 사용하여 인증해야 합니다. 이 페이지에서는 각 요청을 구성하고 서명하는 방법을 설명합니다.
노트
이 서명 알고리즘은 Team API 요청에만 적용됩니다. 심리스 월렛 콜백을 구현하는 경우 콜백 서명 알고리즘이 다릅니다 — 자세한 내용은 콜백 인증을 참조하십시오.
필수 헤더
모든 요청에 다음 세 가지 헤더를 포함해야 합니다:
| 헤더 | 값 |
|---|---|
X-Team-Key | Ruby에서 발급한 team_api_key |
X-Team-Timestamp | 현재 유닉스 타임스탬프(초) (예: 1711500000) |
X-Team-Signature | HMAC-SHA256 서명 (16진수 인코딩) |
서명 구성
1단계 — 경로 문자열 구성
요청 경로를 사용합니다. 요청에 쿼리 스트링이 있는 경우 경로에 추가합니다:
path = "/api/brand/123" # 쿼리 스트링 없음
path = "/api/bet/list?page=1&size=20" # 쿼리 스트링 포함
2단계 — 서명 문자열 조립
정확히 다음 순서로 네 개의 필드를 구분자 없이 연결합니다:
signature_string = {timestamp}{HTTP_METHOD}{path}{raw_body}
| 필드 | 상세 |
|---|---|
timestamp | X-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 요청에 본문 전송 | 예기치 않은 동작 | 본문이 없는 요청에는 빈 문자열 ""을 사용하십시오 |