Skip to content

v2.6 API 认证与鉴权

v2.6 API 支持两种认证方式,建议通过开放平台配置强制使用更安全的 App Key & App Secret 认证方式。

WARNING

v2.6 版本的认证凭证放在 URL 路径中,与 v3 版本不同。

App Key & App Secret 认证

请求示例

bash
curl "https://api.caiyunapp.com/v2.6/{app_key}/{longitude},{latitude}/weather?{query}" \
   -H 'x-cy-nonce: {nonce}' \
   -H 'x-cy-timestamp: {timestamp}' \
   -H 'x-cy-signature: {signature}'

实际示例

bash
# 无 query 参数
curl "https://api.caiyunapp.com/v2.6/your_app_key/116.3176,39.9760/realtime" \
   -H 'x-cy-nonce: 0195c68a-42e7-7243-bff2-ac97a78b837d' \
   -H 'x-cy-timestamp: 1742791910' \
   -H 'x-cy-signature: your_signature_here'

# 有 query 参数
curl "https://api.caiyunapp.com/v2.6/your_app_key/116.3176,39.9760/weather?alert=true&dailysteps=1&hourlysteps=24" \
   -H 'x-cy-nonce: 0195c68a-42e7-7243-bff2-ac97a78b837d' \
   -H 'x-cy-timestamp: 1742791910' \
   -H 'x-cy-signature: your_signature_here'

请求说明

  1. URL 路径参数

    • app_key:从开放平台获取的认证凭证(放在 URL 路径中,替代原来的 token)
    • longitude:经度
    • latitude:纬度
  2. 认证头

    • x-cy-nonce:随机字符串,长度要求 16-40 位,建议使用 UUID,每次请求不可重复
    • x-cy-timestamp:请求时间的时间戳(过旧的时间戳可能会被拒绝请求)
    • x-cy-signature:签名,计算方式见下文
  3. 请求参数

    • method:HTTP 请求方法(当前仅支持 GET)
    • path:API 请求路径(包含 app_key)
    • query:可选的查询参数(如 alert、dailysteps、hourlysteps 等)

签名计算

  1. 参数排序(如果有 query 参数)

    • 将 query 参数按字母顺序排序
    • 示例:hourlysteps=24&dailysteps=1&alert=truealert=true&dailysteps=1&hourlysteps=24
  2. 构建签名字符串

    • 按以下格式拼接:{method}:{path}:{query}:{app_key}:{nonce}:{timestamp}
    • 无 query 参数示例:GET:/v2.6/your_app_key/116.3176,39.9760/realtime::your_app_key:0195c68a-42e7-7243-bff2-ac97a78b837d:1742791910
    • 有 query 参数示例:GET:/v2.6/your_app_key/116.3176,39.9760/weather:alert=true&dailysteps=1&hourlysteps=24:your_app_key:0195c68a-42e7-7243-bff2-ac97a78b837d:1742791910
  3. 计算哈希值

    • 对签名字符串进行 HMAC-SHA256 哈希计算,使用 app_secret 作为密钥
    • 示例:hmac_sha256(stringToSign, app_secret)
  4. 生成签名

    • 使用 URL Safe Base64 对哈希值进行编码
    • 示例:base64(hmac_sha256_result)

示例

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
)

func Sign(
	appKey string,
	appSecret string,
	method string,
	path string,
	nonce string,
	timestamp int64,
	query map[string]string,
) string {
	// 1. 对 query 参数按字母顺序排序
	keys := make([]string, 0, len(query))
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// 2. 构建 query 字符串(URL 编码)
	var queryStr strings.Builder
	for i, k := range keys {
		if i > 0 {
			queryStr.WriteString("&")
		}
		queryStr.WriteString(url.QueryEscape(k))
		queryStr.WriteString("=")
		queryStr.WriteString(url.QueryEscape(query[k]))
	}

	// 3. 构建签名字符串
	stringToSign := strings.Join([]string{
		method,
		path,
		queryStr.String(),
		appKey,
		nonce,
		strconv.FormatInt(timestamp, 10),
	}, ":")

	// 4. 使用 HMAC-SHA256 计算签名
	h := hmac.New(sha256.New, []byte(appSecret))
	h.Write([]byte(stringToSign))

	// 5. Base64 编码(URL 安全)
	signature := base64.URLEncoding.EncodeToString(h.Sum(nil))

	return signature
}

func main() {
	appKey := "your_app_key"
	appSecret := "your_app_secret"
	method := http.MethodGet
	path := "/v2.6/your_app_key/116.3176,39.9760/weather"
	query := map[string]string{
		"alert":       "true",
		"dailysteps":  "1",
		"hourlysteps": "24",
	}
	nonce := "0195c68a-42e7-7243-bff2-ac97a78b837d"
	timestamp := int64(1742791910)

	signature := Sign(appKey, appSecret, method, path, nonce, timestamp, query)
	fmt.Println("Signature:", signature)
  // Signature: KfHsk3z2XfX6Yxox4Uf_VgyM0wHk6bWEyRqZ9QOJUYw=
}
python
import hmac
import hashlib
import base64
import urllib.parse
from typing import Dict

def sign(
    app_key: str,
    app_secret: str,
    method: str,
    path: str,
    nonce: str,
    timestamp: str,
    query: Dict[str, str],
) -> str:
    # 1. 对 query 参数按字母顺序排序
    sorted_keys = sorted(query.keys())

    # 2. 构建 query 字符串(URL 编码,与 Golang 的 url.QueryEscape 对齐)
    query_parts = []
    for k in sorted_keys:
        encoded_key = urllib.parse.quote_plus(k, safe="")
        encoded_value = urllib.parse.quote_plus(query[k], safe="")
        query_parts.append(f"{encoded_key}={encoded_value}")
    query_str = "&".join(query_parts)

    # 3. 构建签名字符串
    string_to_sign = ":".join([method, path, query_str, app_key, nonce, timestamp])

    # 4. 使用 HMAC-SHA256 计算签名
    h = hmac.new(app_secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256)

    # 5. Base64 编码(URL 安全)
    signature = base64.urlsafe_b64encode(h.digest()).decode("utf-8")

    return signature

# 示例使用
app_key = "your_app_key"
app_secret = "your_app_secret"
method = "GET"
path = "/v2.6/your_app_key/116.3176,39.9760/weather"
query = {
    "alert": "true",
    "dailysteps": "1",
    "hourlysteps": "24"
}
nonce = "0195c68a-42e7-7243-bff2-ac97a78b837d"
timestamp = "1742791910"

signature = sign(app_key, app_secret, method, path, nonce, timestamp, query)
print(f"Signature: {signature}")
# Signature: KfHsk3z2XfX6Yxox4Uf_VgyM0wHk6bWEyRqZ9QOJUYw=
javascript
const crypto = require("crypto");

function encodeQueryComponent(input) {
  return encodeURIComponent(String(input))
    .replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`)
    .replace(/%20/g, "+");
}

function sign(appKey, appSecret, method, path, nonce, timestamp, query) {
  // 1. 对 query 参数按字母顺序排序
  const sortedKeys = Object.keys(query).sort();

  // 2. 构建 query 字符串(URL 编码,与 Golang 的 url.QueryEscape 对齐)
  const queryStr = sortedKeys
    .map((key) => `${encodeQueryComponent(key)}=${encodeQueryComponent(query[key])}`)
    .join("&");

  // 3. 构建签名字符串
  const stringToSign = [
    method,
    path,
    queryStr,
    appKey,
    nonce,
    timestamp.toString(),
  ].join(":");

  // 4. 使用 HMAC-SHA256 计算签名
  const hmac = crypto.createHmac("sha256", appSecret);
  hmac.update(stringToSign);

  // 5. Base64 编码(URL 安全)
  const signature = hmac
    .digest("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  return signature;
}

// 示例使用
const appKey = "your_app_key";
const appSecret = "your_app_secret";
const method = "GET";
const path = "/v2.6/your_app_key/116.3176,39.9760/weather";
const query = {
  alert: "true",
  dailysteps: "1",
  hourlysteps: "24",
};
const nonce = "0195c68a-42e7-7243-bff2-ac97a78b837d";
const timestamp = 1742791910;

const signature = sign(appKey, appSecret, method, path, nonce, timestamp, query);
console.log("Signature:", signature);
// Signature: KfHsk3z2XfX6Yxox4Uf_VgyM0wHk6bWEyRqZ9QOJUYw=
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class ApiSignature {

    public static String sign(
        String appKey,
        String appSecret,
        String method,
        String path,
        String nonce,
        long timestamp,
        Map<String, String> query
    ) throws Exception {
        // 1. 对 query 参数按字母顺序排序
        Map<String, String> sortedQuery = new TreeMap<>(query);

        // 2. 构建 query 字符串(URL 编码)
        String queryStr = sortedQuery.entrySet().stream()
            .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) +
                         "=" +
                         URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
            .collect(Collectors.joining("&"));

        // 3. 构建签名字符串
        String stringToSign = String.join(":",
            method,
            path,
            queryStr,
            appKey,
            nonce,
            String.valueOf(timestamp)
        );

        // 4. 使用 HMAC-SHA256 计算签名
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(
            appSecret.getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"
        );
        hmac.init(secretKey);
        byte[] signatureBytes = hmac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));

        // 5. Base64 编码(URL 安全)
        String signature = Base64.getUrlEncoder().encodeToString(signatureBytes);

        return signature;
    }

    public static void main(String[] args) throws Exception {
        String appKey = "your_app_key";
        String appSecret = "your_app_secret";
        String method = "GET";
        String path = "/v2.6/your_app_key/116.3176,39.9760/weather";
        Map<String, String> query = new TreeMap<>();
        query.put("alert", "true");
        query.put("dailysteps", "1");
        query.put("hourlysteps", "24");
        String nonce = "0195c68a-42e7-7243-bff2-ac97a78b837d";
        long timestamp = 1742791910;

        String signature = sign(appKey, appSecret, method, path, nonce, timestamp, query);
        System.out.println("Signature: " + signature);
        // Signature: KfHsk3z2XfX6Yxox4Uf_VgyM0wHk6bWEyRqZ9QOJUYw=
    }
}

完整请求示例

go
package main

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"
)

func sign(
	appKey string,
	appSecret string,
	method string,
	path string,
	nonce string,
	timestamp int64,
	query map[string]string,
) string {
	keys := make([]string, 0, len(query))
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var queryStr strings.Builder
	for i, k := range keys {
		if i > 0 {
			queryStr.WriteString("&")
		}
		queryStr.WriteString(url.QueryEscape(k))
		queryStr.WriteString("=")
		queryStr.WriteString(url.QueryEscape(query[k]))
	}

	stringToSign := strings.Join([]string{
		method,
		path,
		queryStr.String(),
		appKey,
		nonce,
		strconv.FormatInt(timestamp, 10),
	}, ":")

	h := hmac.New(sha256.New, []byte(appSecret))
	h.Write([]byte(stringToSign))
	return base64.URLEncoding.EncodeToString(h.Sum(nil))
}

func main() {
	appKey := "your_app_key"
	appSecret := "your_app_secret"
	lon, lat := "116.3176", "39.9760"

	query := map[string]string{
		"alert":       "true",
		"dailysteps":  "1",
		"hourlysteps": "24",
	}

	b := make([]byte, 16)
	rand.Read(b)
	nonce := hex.EncodeToString(b) // 32 位,满足 16-40 位要求
	timestamp := time.Now().Unix()
	path := fmt.Sprintf("/v2.6/%s/%s,%s/weather", appKey, lon, lat)

	// 构建查询字符串(按字母排序)
	keys := make([]string, 0, len(query))
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var queryParts []string
	for _, k := range keys {
		queryParts = append(queryParts, url.QueryEscape(k)+"="+url.QueryEscape(query[k]))
	}
	rawQuery := strings.Join(queryParts, "&")

	signature := sign(appKey, appSecret, "GET", path, nonce, timestamp, query)

	reqURL := fmt.Sprintf("https://api.caiyunapp.com%s?%s", path, rawQuery)
	fmt.Println("reqURL:", reqURL)
	req, _ := http.NewRequest("GET", reqURL, nil)
	req.Header.Set("x-cy-nonce", nonce)
	req.Header.Set("x-cy-timestamp", strconv.FormatInt(timestamp, 10))
	req.Header.Set("x-cy-signature", signature)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}
python
import hmac
import hashlib
import base64
import urllib.parse
import time
import uuid
import requests
from typing import Dict


def sign(
    app_key: str,
    app_secret: str,
    method: str,
    path: str,
    nonce: str,
    timestamp: str,
    query: Dict[str, str],
) -> str:
    sorted_keys = sorted(query.keys())
    query_parts = [
        f"{urllib.parse.quote_plus(k, safe='')}={urllib.parse.quote_plus(query[k], safe='')}"
        for k in sorted_keys
    ]
    query_str = "&".join(query_parts)
    string_to_sign = ":".join([method, path, query_str, app_key, nonce, timestamp])
    h = hmac.new(app_secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256)
    return base64.urlsafe_b64encode(h.digest()).decode("utf-8")


app_key = "your_app_key"
app_secret = "your_app_secret"
lon, lat = "116.3176", "39.9760"

query = {"alert": "true", "dailysteps": "1", "hourlysteps": "24"}
nonce = str(uuid.uuid4())
timestamp = str(int(time.time()))
path = f"/v2.6/{app_key}/{lon},{lat}/weather"

signature = sign(app_key, app_secret, "GET", path, nonce, timestamp, query)

resp = requests.get(
    f"https://api.caiyunapp.com{path}",
    params=query,
    headers={
        "x-cy-nonce": nonce,
        "x-cy-timestamp": timestamp,
        "x-cy-signature": signature,
    },
)
print(resp.json())
javascript
const crypto = require("crypto");
const https = require("https");
const { v4: uuidv4 } = require("uuid");

function encodeQueryComponent(input) {
  return encodeURIComponent(String(input))
    .replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`)
    .replace(/%20/g, "+");
}

function sign(appKey, appSecret, method, path, nonce, timestamp, query) {
  const sortedKeys = Object.keys(query).sort();
  const queryStr = sortedKeys
    .map((key) => `${encodeQueryComponent(key)}=${encodeQueryComponent(query[key])}`)
    .join("&");
  const stringToSign = [method, path, queryStr, appKey, nonce, timestamp.toString()].join(":");
  const hmac = crypto.createHmac("sha256", appSecret);
  hmac.update(stringToSign);
  return hmac.digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
}

const appKey = "your_app_key";
const appSecret = "your_app_secret";
const lon = "116.3176";
const lat = "39.9760";
const query = { alert: "true", dailysteps: "1", hourlysteps: "24" };

const nonce = uuidv4();
const timestamp = Math.floor(Date.now() / 1000);
const path = `/v2.6/${appKey}/${lon},${lat}/weather`;
const signature = sign(appKey, appSecret, "GET", path, nonce, timestamp, query);

const queryStr = Object.keys(query)
  .sort()
  .map((k) => `${encodeQueryComponent(k)}=${encodeQueryComponent(query[k])}`)
  .join("&");

const options = {
  hostname: "api.caiyunapp.com",
  path: `${path}?${queryStr}`,
  method: "GET",
  headers: {
    "x-cy-nonce": nonce,
    "x-cy-timestamp": timestamp.toString(),
    "x-cy-signature": signature,
  },
};

const req = https.request(options, (res) => {
  let data = "";
  res.on("data", (chunk) => (data += chunk));
  res.on("end", () => console.log(JSON.parse(data)));
});
req.end();
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.Collectors;

public class ApiRequest {

    public static String sign(
        String appKey,
        String appSecret,
        String method,
        String path,
        String nonce,
        long timestamp,
        Map<String, String> query
    ) throws Exception {
        Map<String, String> sortedQuery = new TreeMap<>(query);
        String queryStr = sortedQuery.entrySet().stream()
            .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
                      + "="
                      + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
            .collect(Collectors.joining("&"));

        String stringToSign = String.join(":",
            method, path, queryStr, appKey, nonce, String.valueOf(timestamp));

        Mac hmac = Mac.getInstance("HmacSHA256");
        hmac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signatureBytes = hmac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        return Base64.getUrlEncoder().encodeToString(signatureBytes);
    }

    public static void main(String[] args) throws Exception {
        String appKey = "your_app_key";
        String appSecret = "your_app_secret";
        String lon = "116.3176";
        String lat = "39.9760";

        Map<String, String> query = new TreeMap<>();
        query.put("alert", "true");
        query.put("dailysteps", "1");
        query.put("hourlysteps", "24");

        String nonce = UUID.randomUUID().toString();
        long timestamp = Instant.now().getEpochSecond();
        String path = "/v2.6/" + appKey + "/" + lon + "," + lat + "/weather";

        String signature = sign(appKey, appSecret, "GET", path, nonce, timestamp, query);

        String queryStr = query.entrySet().stream()
            .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
                      + "="
                      + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
            .collect(Collectors.joining("&"));

        URL url = new URL("https://api.caiyunapp.com" + path + "?" + queryStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("x-cy-nonce", nonce);
        conn.setRequestProperty("x-cy-timestamp", String.valueOf(timestamp));
        conn.setRequestProperty("x-cy-signature", signature);

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            reader.lines().forEach(System.out::println);
        }
    }
}

Token 认证(不建议)

WARNING

Token 认证方式有泄漏风险,建议使用 App Key & App Secret 认证方式。

在 v2.6 版本中,Token 直接放在 URL 路径中:

bash
curl "https://api.caiyunapp.com/v2.6/{token}/{longitude},{latitude}/realtime"

实际示例

bash
curl "https://api.caiyunapp.com/v2.6/TAkhjf8d1nlSlspN/101.6656,39.2072/realtime"