Skip to content

API Signature Authentication


API Signature Algorithm

Elements Involved in Signature Calculation

Parameter Description
method HTTP request method. Currently supported methods are GET and POST.
nonce Random number, corresponding to the X-Df-Nonce request header.
timestamp Timestamp of the request initiation time, in seconds, corresponding to the X-Df-Timestamp request header.
path Original request path (including the query string). For example:
GET request: /api/v1/account/list?pageIndex=1&pageSize=20
POST request: /api/v1/account/add
body Original body of the request. When there is no body, the body defaults to an empty string and participates in the signature calculation.

Signature Calculation Logic

Concatenate method (uppercase), nonce (random temporary code generated at request time), path (original request path and query string), timestamp, and body with a single space to form the signature original string (string_to_sign). When there is no body, the body defaults to an empty string.

That is, the signature original string string_to_sign = '{method} {nonce} {path} {timestamp} {body}'.

Use the secretKey described in the "Service Configuration" section as the key to encrypt (hmac sha256) the string_to_sign, obtaining the authentication signature for each request, which is the value of the X-Signature request header.

Python Signature and API Request Example

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import time
import hmac
import traceback
import uuid
import hashlib
from urllib.parse import urlencode
import requests

class DFExternalAPI(object):

    def __init__(self, server, access_key, secret_key, debug=False):
        self.server = server
        self.access_key = access_key
        self.secret_key = secret_key
        self.debug = debug

    def get_sign(self, method, full_path, timestamp, nonce, body=None):
        """ Generate signature """
        if body is None:
            # When there is no body, set body to an empty string by default
            body = ""
        # Original signature string
        string_to_sign = " ".join([method, nonce, full_path, timestamp, body])
        # Encode the signature string
        string_to_sign = string_to_sign.encode(encoding="utf-8")
        secret_key = self.secret_key.encode(encoding="utf-8")
        # Perform hmac encryption
        h = hmac.new(secret_key, string_to_sign, hashlib.sha256)
        sign = h.hexdigest()
        return sign

    def get_auth_header(self, method, full_path, body, filepath):
        """ Generate request header information """
        # Timestamp of the current time
        timestamp = str(int(time.time()))
        # Random number
        nonce = uuid.uuid4().hex
        # Generate signature
        sign = self.get_sign(method, full_path, timestamp, nonce, body) if self.access_key else None
        # Generate complete request header information
        auth_headers = {
            'Content-Type': 'application/json',
            'X-Df-Access-Key': self.access_key,
            'X-Df-Timestamp': timestamp,
            'X-Df-Nonce': nonce,
            'X-Df-SVersion': "v20240417",
            'X-Df-Signature': sign,
        }
        if filepath:
            # Note: file uploads are done via form, so Content-Type is not specified here, but generated internally during the request
            auth_headers.pop("Content-Type", "")
            # = "multipart/form-data"
        return auth_headers

    def run(self, method, path, query=None, body=None, headers=None, filepath=None, **kwargs):
        """ Execute request and extract response information """
        # Format request method
        method = method.upper()
        # Generate complete request path information
        if query:
            path = path + '?' + urlencode(query, encoding='utf-8')
        # The body needs to be encoded as unicode (key sorting is not mandatory, just ensure the final body is unicode encoded)
        if body:
            if isinstance(body, (tuple, list, dict)):
                body = json.dumps(body, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
        else:
            body = ""
        # Merge request headers
        headers = headers or {}
        new_headers = self.get_auth_header(method, path, body, filepath)
        new_headers.update(headers)
        # Complete request URL
        url = self.server + path
        if self.debug:
            print("============Request Details==================")
            print("url: ", url)
            print("---- headers ----")
            print(json.dumps(headers, indent=4, ensure_ascii=False))
            print("---- body ----")
            print(json.dumps(body, indent=4, ensure_ascii=False))
            print("==============================")
        # Execute request
        files = None
        if filepath:
            files = {'file': open(filepath, 'rb')}
        try:
            resp = requests.__getattribute__(method.lower())(
                url,
                data=body.encode(encoding='utf-8'),
                headers=new_headers,
                files=files
            )
        except Exception as e:
            print(traceback.format_exc())
            raise e
        finally:
            if files:
                files["file"].close()

        # Get response status
        resp_status_code = resp.status_code
        # Response content
        resp_data = resp.text
        if resp_status_code != 200:
            raise Exception(resp_data)

        resp_content_type = resp.headers.get('Content-Type')
        if isinstance(resp_content_type, str):
            resp_content_type = resp_content_type.split(';')[0].strip()

        if resp_content_type == 'application/json':
            try:
                resp_data = json.loads(resp_data)
            except Exception as e:
                raise Exception('Cannot parse response body as json', resp_data)

        return resp_status_code, resp_data

    def get(self, path, query=None, headers=None):
        return self.run('GET', path, query, None, headers)

    def post(self, path, query=None, body=None, headers=None, **kwargs):
        return self.run('POST', path, query, body, headers, **kwargs)

    def account_list(self, search="", page_index=1, page_size=10, **kwargs):
        path = "/api/v1/account/list"
        query = {
            "search": search,
            "pageIndex": page_index,
            "pageSize": page_size
        }
        return self.get(path, query)

    def query_data(self, workspace_uuid, body, **kwargs):
        path = f"/api/v1/df/{workspace_uuid}/query_data"
        return self.post(path, body=body)

    def upload_logo_image(self, workspace_uuid, filename, filepath, **kwargs):
        path = f"/api/v1/workspace/{workspace_uuid}/upload_logo_image"
        query = {
            "filename": filename,
            "language": "zh",
        }

        return self.post(path, query=query, filepath=filepath)

    def get_logo_url(self, workspace_uuid, filename, **kwargs):
        path = f"/api/v1/workspace/{workspace_uuid}/get_logo_url"
        query = {
            "filename": filename,
            "language": "zh",
        }
        return self.post(path, query=query)

def test_get(server, access_key, secret_key):

    dfapi = DFExternalAPI(server, access_key, secret_key)
    status_code, resp = dfapi.account_list("test")
    print(f"======={__name__}====")
    print(f"status_code: {status_code}")
    print(f"resp: {resp}")

def test_post(server, access_key, secret_key):
    dfapi = DFExternalAPI(server, access_key, secret_key, True)
    workspace_uuid = "wksp_4b57c7bab38e4a2d9630f675dc20015d"
    body = {
        "queries": [
            {
                "qtype": "dql",
                "query": {
                    "q": "L::re(`.*`):(fill(count(__docid), 0) as `count`){ (`index` IN ['default']) AND (`message` = queryString(\"Guance\")) } BY `status`",
                    "funcList": [],
                    "maxPointCount": 60,
                    "interval": 20,
                    "align_time": True,
                    "sorder_by": [{"column": "`#1`", "order": "DESC"}],
                    "slimit": 20,
                    "disable_sampling": False,
                    "timeRange": [1713440394537, 1713441294537],
                    "tz": "Asia/Shanghai"
                }
            }
        ]
    }
    status_code, resp = dfapi.query_data(workspace_uuid, body)
    print(f"======={__name__}====")
    print(f"status_code: {status_code}")
    print(f"resp: {resp}")

def test_upload_logo_image(server, access_key, secret_key):
    dfapi = DFExternalAPI(server, access_key, secret_key, True)
    workspace_uuid = "wksp_4b57c7bab38e4a2d9630f675dc20015d"
    filename = "logo.png"
    filepath = "/Users/luwenchang/Downloads/logo/parrot1.jpeg"
    filename = "favicon.ico"
    filepath = "/Users/luwenchang/Downloads/logo/rabbit4.jpeg"
    status_code, resp = dfapi.upload_logo_image(workspace_uuid, filename, filepath)
    print(f"======={__name__}====")
    print(f"status_code: {status_code}")
    print(f"resp: {resp}")

def test_get_logo_url(server, access_key, secret_key):
    dfapi = DFExternalAPI(server, access_key, secret_key, True)
    workspace_uuid = "wksp_4b57c7bab38e4a2d9630f675dc20015d"
    filename = "logo.png"
    status_code, resp = dfapi.get_logo_url(workspace_uuid, filename)
    print(f"======={__name__}====")
    print(f"status_code: {status_code}")
    print(f"resp: {resp}")


if __name__ == '__main__':
    # External API service domain address
    server = "http://127.0.0.1:5000"
    # Configured access key (ak)
    access_key = "abcd"
    # Configured secret key (sk)
    secret_key = "Admin123"
    # test_get(server, access_key, secret_key)
    # test_post(server, access_key, secret_key)
    test_upload_logo_image(server, access_key, secret_key)
    # test_get_logo_url(server, access_key, secret_key)

Go Signature and API Request Example

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/json"
    "net/url"

    //"encoding/base64"
    "encoding/hex"
    "fmt"
    "github.com/google/uuid"
    "io/ioutil"
    "net/http"
    "strconv"
    "time"
)

type DFExternalAPI struct {
    server     string
    accessKey  string
    secretKey  string
    debug      bool
}

func Marshal(data interface{}) []byte {
    byteStr, err := json.Marshal(data)
    if err != nil {
        fmt.Println("JsonMarshal error:", err)
    }

    return byteStr
}

func NewDFExternalAPI(server, accessKey, secretKey string, debug bool) *DFExternalAPI {
    return &DFExternalAPI{
        server:    server,
        accessKey: accessKey,
        secretKey: secretKey,
        debug:     debug,
    }
}

func (api *DFExternalAPI) GetSign(method, fullPath string, timestamp, nonce string, body string) string {
    if body == "" {
        body = ""
    }

    stringToSign := fmt.Sprintf("%s %s %s %s %s", method, nonce, fullPath, timestamp, body)
    stringToSignBytes := []byte(stringToSign)
    //
    secretKeyBytes := []byte(api.secretKey)

    h := hmac.New(sha256.New, secretKeyBytes)
    h.Write(stringToSignBytes)
    sign := hex.EncodeToString(h.Sum(nil))

    //hmacGen := hmac.New(sha256.New, []byte(api.secretKey))
    //hmacGen.Write([]byte(stringToSign))
    //sign := base64.StdEncoding.EncodeToString(hmacGen.Sum(nil))

    return sign
}

func (api *DFExternalAPI) SetDFHeaders(req *http.Request, timestamp, nonce, sign string)  {
    // Set request headers
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Df-Access-Key", api.accessKey)
    req.Header.Set("X-Df-Timestamp", timestamp)
    req.Header.Set("X-Df-Nonce", nonce)
    req.Header.Set("X-Df-SVersion", "v20240417")
    req.Header.Set("X-Df-Signature", sign)
    return
}

func eTestGet(server, accessKey, secretKey string) {
    // Generate a random UUID and format it as a string
    nonce := uuid.New().String()
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    //fullPath := "/api/v1/account/list?search=China&pageIndex=1&pageSize=10"
    v := url.Values{}
    v.Set("search", "China")
    v.Set("pageIndex", "1")
    v.Set("pageSize", "10")

    fullPath := fmt.Sprintf("/api/v1/account/list?%s", v.Encode())

    api := NewDFExternalAPI(server, accessKey, secretKey, false)
    sign := api.GetSign("GET", fullPath, timestamp, nonce, "")

    url := api.server + fullPath
    // Create a GET request
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Println("Failed to create request:", err)
        return
    }
    api.SetDFHeaders(req, timestamp, nonce, sign)
    // Send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    defer resp.Body.Close()

    // Read response body
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Failed to read response body:", err)
        return
    }

    // Print response body
    fmt.Println(string(body))
}

func eTestPost(server, accessKey, secretKey string) {
    method := "POST"
    // Generate a random UUID and format it as a string
    nonce := uuid.New().String()
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    workspaceUUID := "wksp_4b57c7bab38e4a2d9630f675dc20015d"
    fullPath := fmt.Sprintf("/api/v1/df/%s/query_data", workspaceUUID)

    api := NewDFExternalAPI(server, accessKey, secretKey, false)

    body := map[string]interface{}{
        "queries": []map[string]interface{}{
            {
                "uuid": "15d0fb90-fd81-11ee-b9b6-d19f81405637",
                "qtype": "dql",
                "query": map[string]interface{}{
                    "q":             "L::re(`.*`):(fill(count(__docid), 0) as `count`){ (`index` IN ['default']) AND (`message` = queryString(\"Guance\")) } BY `status`",
                    "_funcList":     []interface{}{},
                    "funcList":      []interface{}{},
                    "maxPointCount": 60,
                    "interval":      20,
                    "align_time":    true,
                    "sorder_by": []map[string]interface{}{
                        {
                            "column": "`#1`",
                            "order":  "DESC",
                        },
                    },
                    "slimit":            20,
                    "disable_sampling":  false,
                    "timeRange":         []int64{1713440394537, 1713441294537},
                    "tz":                "Asia/Shanghai",
                },
            },
        },
    }
    bodyStr := Marshal(body)
    sign := api.GetSign(method, fullPath, timestamp, nonce, string(bodyStr))

    url := api.server + fullPath
    // Create a GET request
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyStr))
    if err != nil {
        fmt.Println("Failed to create request:", err)
        return
    }
    //// Set request headers
    api.SetDFHeaders(req, timestamp, nonce, sign)
    // Send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    defer resp.Body.Close()
    // Read response body
    content, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Failed to read response body:", err)
        return
    }
    // Print response body
    fmt.Println(string(content))
}

func main() {
    accessKey := "abcd"
    secretKey := "Admin123"
    server := "http://127.0.0.1:5000"
    eTestGet(server, accessKey, secretKey)
    eTestPost(server, accessKey, secretKey)
}

Feedback

Is this page helpful?