Skip to content

快速示例 ​

以下示例都采用同一模式:

  • 内存中只维护一个“最新令牌变量”。
  • 每次换新成功后覆盖该变量。
  • 业务调用统一从 getApiKey()/GetApiKey()/get_api_key() 读取当前令牌。

Java Spring(Boot 3,RestTemplate) ​

java
package com.example.vdp;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

@Service
public class VdpTokenService {
    private static final long REFRESH_WINDOW_SECONDS = 300;
    private static final long SKEW_SECONDS = 30;

    @Value("${vdp.base-url:https://open-global.vdsentnet.com}")
    private String baseUrl;
    @Value("${vdp.client-id}")
    private String clientId;
    @Value("${vdp.client-secret}")
    private String clientSecret;

    private final RestTemplate restTemplate;
    private final AtomicReference<String> currentToken = new AtomicReference<>("");
    private final AtomicLong expireAtEpochSeconds = new AtomicLong(0);

    public VdpTokenService(RestTemplateBuilder builder) {
        this.restTemplate = builder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(10))
                .build();
    }

    public synchronized String getApiKey() {
        long now = System.currentTimeMillis() / 1000;
        String token = currentToken.get();
        long expireAt = expireAtEpochSeconds.get();

        if (token.isBlank() || now >= expireAt) {
            exchangeToken();
            return currentToken.get();
        }

        long left = expireAt - now;
        if (left <= REFRESH_WINDOW_SECONDS) {
            boolean refreshed = refreshToken(token);
            if (!refreshed && now >= expireAtEpochSeconds.get()) {
                exchangeToken();
            }
        }
        return currentToken.get();
    }

    private void exchangeToken() {
        String url = baseUrl + "/api/auth/token";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, String> body = Map.of("clientId", clientId, "clientSecret", clientSecret);

        ResponseEntity<Map> resp = restTemplate.postForEntity(url, new HttpEntity<>(body, headers), Map.class);
        updateLocalToken(resp.getBody());
    }

    private boolean refreshToken(String oldToken) {
        String url = baseUrl + "/api/auth/token/refresh";
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(oldToken);

        try {
            ResponseEntity<Map> resp = restTemplate.exchange(
                    url,
                    HttpMethod.POST,
                    new HttpEntity<>(headers),
                    Map.class
            );
            updateLocalToken(resp.getBody());
            return true;
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode() == HttpStatus.BAD_REQUEST &&
                    Objects.requireNonNullElse(e.getResponseBodyAsString(), "").contains("RefreshTooEarly")) {
                return false;
            }
            return false;
        }
    }

    private void updateLocalToken(Map body) {
        if (body == null) throw new IllegalStateException("Empty token response");
        String token = String.valueOf(body.get("accessToken"));
        Number expiresIn = (Number) body.get("expiresInSeconds");
        long now = System.currentTimeMillis() / 1000;
        long expireAt = now + expiresIn.longValue() - SKEW_SECONDS;

        currentToken.set(token);
        expireAtEpochSeconds.set(expireAt);
    }
}

Node.js(18+,原生 fetch) ​

js
const VDP_BASE_URL = process.env.VDP_BASE_URL || "https://open-global.vdsentnet.com";
const CLIENT_ID = process.env.VDP_CLIENT_ID || "your_client_id";
const CLIENT_SECRET = process.env.VDP_CLIENT_SECRET || "your_client_secret";

const REFRESH_WINDOW_MS = 5 * 60 * 1000;
const SKEW_MS = 30 * 1000;

let currentToken = "";
let expireAtMs = 0;

async function exchangeToken() {
  const resp = await fetch(`${VDP_BASE_URL}/api/auth/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: CLIENT_ID,
      clientSecret: CLIENT_SECRET,
    }),
  });

  if (!resp.ok) throw new Error(`exchangeToken failed: ${resp.status}`);
  const data = await resp.json();
  currentToken = data.accessToken;
  expireAtMs = Date.now() + data.expiresInSeconds * 1000 - SKEW_MS;
}

async function refreshToken() {
  const resp = await fetch(`${VDP_BASE_URL}/api/auth/token/refresh`, {
    method: "POST",
    headers: { Authorization: `Bearer ${currentToken}` },
  });

  if (resp.status === 400) {
    const err = await resp.json().catch(() => ({}));
    if (err.error === "RefreshTooEarly") return false;
  }
  if (!resp.ok) return false;

  const data = await resp.json();
  currentToken = data.accessToken;
  expireAtMs = Date.now() + data.expiresInSeconds * 1000 - SKEW_MS;
  return true;
}

export async function getApiKey() {
  if (!currentToken || Date.now() >= expireAtMs) {
    await exchangeToken();
    return currentToken;
  }

  if (expireAtMs - Date.now() <= REFRESH_WINDOW_MS) {
    const ok = await refreshToken();
    if (!ok && Date.now() >= expireAtMs) {
      await exchangeToken();
    }
  }
  return currentToken;
}

Go(1.22+,标准库) ​

go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
	"time"
)

type TokenManager struct {
	BaseURL      string
	ClientID     string
	ClientSecret string
	HttpClient   *http.Client

	mu       sync.Mutex
	token    string
	expireAt time.Time
}

const refreshWindow = 5 * time.Minute
const skew = 30 * time.Second

func NewTokenManager() *TokenManager {
	return &TokenManager{
		BaseURL:      getenv("VDP_BASE_URL", "https://open-global.vdsentnet.com"),
		ClientID:     getenv("VDP_CLIENT_ID", "your_client_id"),
		ClientSecret: getenv("VDP_CLIENT_SECRET", "your_client_secret"),
		HttpClient:   &http.Client{Timeout: 10 * time.Second},
	}
}

func (m *TokenManager) GetAPIKey() (string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	now := time.Now()
	if m.token == "" || now.After(m.expireAt) {
		if err := m.exchangeToken(); err != nil {
			return "", err
		}
		return m.token, nil
	}

	if m.expireAt.Sub(now) <= refreshWindow {
		ok, err := m.refreshToken()
		if err != nil {
			return "", err
		}
		if !ok && time.Now().After(m.expireAt) {
			if err := m.exchangeToken(); err != nil {
				return "", err
			}
		}
	}
	return m.token, nil
}

func (m *TokenManager) exchangeToken() error {
	payload := map[string]string{
		"clientId":     m.ClientID,
		"clientSecret": m.ClientSecret,
	}
	data, _ := json.Marshal(payload)
	req, _ := http.NewRequest("POST", m.BaseURL+"/api/auth/token", bytes.NewReader(data))
	req.Header.Set("Content-Type", "application/json")

	resp, err := m.HttpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("exchange failed: %d %s", resp.StatusCode, string(b))
	}

	return m.updateFromResp(resp.Body)
}

func (m *TokenManager) refreshToken() (bool, error) {
	req, _ := http.NewRequest("POST", m.BaseURL+"/api/auth/token/refresh", nil)
	req.Header.Set("Authorization", "Bearer "+m.token)

	resp, err := m.HttpClient.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == 400 {
		body, _ := io.ReadAll(resp.Body)
		if bytes.Contains(body, []byte("RefreshTooEarly")) {
			return false, nil
		}
	}
	if resp.StatusCode >= 300 {
		return false, nil
	}

	if err := m.updateFromResp(resp.Body); err != nil {
		return false, err
	}
	return true, nil
}

func (m *TokenManager) updateFromResp(r io.Reader) error {
	var resp struct {
		AccessToken      string `json:"accessToken"`
		ExpiresInSeconds int64  `json:"expiresInSeconds"`
	}
	if err := json.NewDecoder(r).Decode(&resp); err != nil {
		return err
	}
	m.token = resp.AccessToken
	m.expireAt = time.Now().Add(time.Duration(resp.ExpiresInSeconds)*time.Second - skew)
	return nil
}

func getenv(k, fallback string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return fallback
}

func main() {
	m := NewTokenManager()
	apiKey, err := m.GetAPIKey()
	if err != nil {
		panic(err)
	}
	fmt.Println("current apiKey:", apiKey[:16], "...")
}

Python(3.10+,requests) ​

python
import os
import time
import requests

VDP_BASE_URL = os.getenv("VDP_BASE_URL", "https://open-global.vdsentnet.com")
CLIENT_ID = os.getenv("VDP_CLIENT_ID", "your_client_id")
CLIENT_SECRET = os.getenv("VDP_CLIENT_SECRET", "your_client_secret")

REFRESH_WINDOW_SECONDS = 300
SKEW_SECONDS = 30

current_token = ""
expire_at = 0


def exchange_token():
    global current_token, expire_at
    resp = requests.post(
        f"{VDP_BASE_URL}/api/auth/token",
        json={"clientId": CLIENT_ID, "clientSecret": CLIENT_SECRET},
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()
    current_token = data["accessToken"]
    expire_at = int(time.time()) + int(data["expiresInSeconds"]) - SKEW_SECONDS


def refresh_token():
    global current_token, expire_at
    resp = requests.post(
        f"{VDP_BASE_URL}/api/auth/token/refresh",
        headers={"Authorization": f"Bearer {current_token}"},
        timeout=10,
    )

    if resp.status_code == 400:
        err = resp.json()
        if err.get("error") == "RefreshTooEarly":
            return False

    if not resp.ok:
        return False

    data = resp.json()
    current_token = data["accessToken"]
    expire_at = int(time.time()) + int(data["expiresInSeconds"]) - SKEW_SECONDS
    return True


def get_api_key():
    global current_token, expire_at
    now = int(time.time())

    if not current_token or now >= expire_at:
        exchange_token()
        return current_token

    if (expire_at - now) <= REFRESH_WINDOW_SECONDS:
        ok = refresh_token()
        if not ok and int(time.time()) >= expire_at:
            exchange_token()

    return current_token

四语言验证调用(HelloWorld) ​

验证接口路径:

  • GET /api/proxy/base/hello-world

Java Spring ​

java
public String verifyHelloWorld() {
    String apiKey = getApiKey();

    HttpHeaders headers = new HttpHeaders();
    headers.set("X-Api-Key", apiKey);

    ResponseEntity<String> resp = restTemplate.exchange(
            baseUrl + "/api/proxy/base/hello-world",
            HttpMethod.GET,
            new HttpEntity<>(headers),
            String.class
    );
    return resp.getBody();
}

Node.js ​

js
export async function verifyHelloWorld() {
  const apiKey = await getApiKey();
  const resp = await fetch(`${VDP_BASE_URL}/api/proxy/base/hello-world`, {
    headers: { "X-Api-Key": apiKey },
  });
  if (!resp.ok) throw new Error(`verify failed: ${resp.status}`);
  return await resp.json();
}

Go ​

go
func VerifyHelloWorld(m *TokenManager) (string, error) {
	apiKey, err := m.GetAPIKey()
	if err != nil {
		return "", err
	}

	req, _ := http.NewRequest("GET", m.BaseURL+"/api/proxy/base/hello-world", nil)
	req.Header.Set("X-Api-Key", apiKey)

	resp, err := m.HttpClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return "", fmt.Errorf("verify failed: %d %s", resp.StatusCode, string(body))
	}
	return string(body), nil
}

Python ​

python
def verify_hello_world():
    api_key = get_api_key()
    resp = requests.get(
        f"{VDP_BASE_URL}/api/proxy/base/hello-world",
        headers={"X-Api-Key": api_key},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()

cURL ​

首次交换签名 ​

bash
curl -X POST "https://open-global.vdsentnet.com/api/auth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "your_app_id",
    "clientSecret": "your_app_secret"
  }'

旧令牌换新(5 分钟窗口内) ​

bash
curl -X POST "https://open-global.vdsentnet.com/api/auth/token/refresh" \
  -H "Authorization: Bearer <currentAccessToken>"

调用代理接口 ​

bash
curl -X GET "https://open-global.vdsentnet.com/api/proxy/base/hello-world" \
  -H "X-Api-Key: <apiKey>"

镜像自 developer.vds.pub/docs