主题
快速示例
以下示例都采用同一模式:
- 内存中只维护一个“最新令牌变量”。
- 每次换新成功后覆盖该变量。
- 业务调用统一从
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>"