API 驗證與簽章
每個請求都必須使用環境專屬憑證,對完整請求內容進行簽章。
必要標頭
| Header | 說明 |
|---|---|
X-API-Key | FREONE 核發的 API Key |
TIMESTAMP | 毫秒時間戳或 ISO-8601 格式;其原始值會納入簽章 |
SIGNATURE | HMAC-SHA256 結果的小寫十六進位字串 |
每次送出請求前都應重新產生 TIMESTAMP 與 SIGNATURE,不要重複使用之前請求的值。
保護 Secret
API Secret 不得放在前端程式碼、日誌、版本控制或未授權的通訊內容中。簽章必須在可信任的後端服務產生。
建立簽章內容(Canonical message)
依下列順序以換行字元 \n 串接五個值;即使 query string 或 body 為空,也必須保留該空行。
TIMESTAMP
HTTP_METHOD
REQUEST_PATH
CANONICAL_QUERY
REQUEST_BODY
HTTP_METHOD必須為大寫,例如GET或POST。REQUEST_PATH只放 API 路徑,例如/v1/merchants;不要放入https://api.developer.freone.com或?name=value。- Query 參數名稱按字典順序排列;同名參數值也按字典順序排列後以逗號串接,再以換行串接各組
name=value。 REQUEST_BODY必須與實際送出的 body 字串完全相同。
例如請求網址的 Query 是 ?z=2&z=1&empty=,CANONICAL_QUERY 應寫成:
empty=
z=1,2
完整請求範例
以下範例皆從環境變數讀取 FREONE_API_KEY 與 FREONE_API_SECRET,並呼叫 Sandbox 的 GET /v1/merchants。
- Node.js
- Python
- PHP
- Java
- C#
- Go
import {createHmac} from 'node:crypto';
const apiKey = process.env.FREONE_API_KEY;
const apiSecret = process.env.FREONE_API_SECRET;
if (!apiKey || !apiSecret) throw new Error('Missing FREONE credentials');
const timestamp = Date.now().toString();
const method = 'GET';
const requestPath = '/v1/merchants';
const canonicalMessage = [timestamp, method, requestPath, '', ''].join('\n');
const signature = createHmac('sha256', apiSecret)
.update(canonicalMessage, 'utf8')
.digest('hex');
const response = await fetch(
`https://api.developer-beta.freone.com${requestPath}`,
{
headers: {
'X-API-Key': apiKey,
TIMESTAMP: timestamp,
SIGNATURE: signature,
},
},
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
console.log(await response.json());
import hashlib
import hmac
import json
import os
import time
import urllib.request
api_key = os.environ["FREONE_API_KEY"]
api_secret = os.environ["FREONE_API_SECRET"]
timestamp = str(int(time.time() * 1000))
method = "GET"
request_path = "/v1/merchants"
canonical_message = "\n".join([timestamp, method, request_path, "", ""])
signature = hmac.new(
api_secret.encode("utf-8"),
canonical_message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
request = urllib.request.Request(
f"https://api.developer-beta.freone.com{request_path}",
method=method,
headers={
"X-API-Key": api_key,
"TIMESTAMP": timestamp,
"SIGNATURE": signature,
},
)
with urllib.request.urlopen(request) as response:
print(json.load(response))
<?php
$apiKey = getenv('FREONE_API_KEY');
$apiSecret = getenv('FREONE_API_SECRET');
if ($apiKey === false || $apiSecret === false) {
throw new RuntimeException('Missing FREONE credentials');
}
$timestamp = (string) round(microtime(true) * 1000);
$method = 'GET';
$requestPath = '/v1/merchants';
$canonicalMessage = implode("\n", [$timestamp, $method, $requestPath, '', '']);
$signature = hash_hmac('sha256', $canonicalMessage, $apiSecret);
$headers = implode("\r\n", [
"X-API-Key: {$apiKey}",
"TIMESTAMP: {$timestamp}",
"SIGNATURE: {$signature}",
]);
$context = stream_context_create([
'http' => ['method' => $method, 'header' => $headers],
]);
$response = file_get_contents(
"https://api.developer-beta.freone.com{$requestPath}",
false,
$context,
);
if ($response === false) {
throw new RuntimeException('FREONE request failed');
}
print_r(json_decode($response, true, flags: JSON_THROW_ON_ERROR));
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.time.Instant;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class FreoneExample {
public static void main(String[] args) throws Exception {
var apiKey = System.getenv("FREONE_API_KEY");
var apiSecret = System.getenv("FREONE_API_SECRET");
if (apiKey == null || apiSecret == null) {
throw new IllegalStateException("Missing FREONE credentials");
}
var timestamp = Long.toString(Instant.now().toEpochMilli());
var method = "GET";
var requestPath = "/v1/merchants";
var canonicalMessage = String.join(
"\n", timestamp, method, requestPath, "", "");
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(
apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
var signature = HexFormat.of().formatHex(
mac.doFinal(canonicalMessage.getBytes(StandardCharsets.UTF_8)));
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.developer-beta.freone.com" + requestPath))
.header("X-API-Key", apiKey)
.header("TIMESTAMP", timestamp)
.header("SIGNATURE", signature)
.GET()
.build();
var response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
var apiKey = Environment.GetEnvironmentVariable("FREONE_API_KEY")
?? throw new InvalidOperationException("Missing FREONE_API_KEY");
var apiSecret = Environment.GetEnvironmentVariable("FREONE_API_SECRET")
?? throw new InvalidOperationException("Missing FREONE_API_SECRET");
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
var method = "GET";
var requestPath = "/v1/merchants";
var canonicalMessage = string.Join(
"\n", new[] { timestamp, method, requestPath, "", "" });
var signature = Convert.ToHexString(HMACSHA256.HashData(
Encoding.UTF8.GetBytes(apiSecret),
Encoding.UTF8.GetBytes(canonicalMessage)
)).ToLowerInvariant();
using var client = new HttpClient();
using var request = new HttpRequestMessage(
HttpMethod.Get,
$"https://api.developer-beta.freone.com{requestPath}"
);
request.Headers.Add("X-API-Key", apiKey);
request.Headers.Add("TIMESTAMP", timestamp);
request.Headers.Add("SIGNATURE", signature);
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
apiKey := os.Getenv("FREONE_API_KEY")
apiSecret := os.Getenv("FREONE_API_SECRET")
if apiKey == "" || apiSecret == "" {
panic("missing FREONE credentials")
}
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
method := http.MethodGet
requestPath := "/v1/merchants"
canonicalMessage := strings.Join(
[]string{timestamp, method, requestPath, "", ""}, "\n")
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(canonicalMessage))
signature := hex.EncodeToString(mac.Sum(nil))
request, err := http.NewRequest(
method,
"https://api.developer-beta.freone.com"+requestPath,
nil,
)
if err != nil {
panic(err)
}
request.Header.Set("X-API-Key", apiKey)
request.Header.Set("TIMESTAMP", timestamp)
request.Header.Set("SIGNATURE", signature)
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
fmt.Println(response.Status)
io.Copy(os.Stdout, response.Body)
}
常見檢查項目
- 簽章所用的 HTTP method 是否已轉為大寫。
- 路徑是否包含
/v1,且沒有帶入網域或?後方的查詢參數。 - Query 排序與同名參數合併方式是否正確。
- 簽章使用的 Body 字串,是否與實際送出的內容完全相同。
- API Key 與 Secret 是否屬於目前呼叫的環境。