Information Security 33 min read

Designing Secure Third‑Party API Authentication with AK/SK, Signatures and Token Mechanisms

The article presents a comprehensive design for securing third‑party API calls by generating unique Access Key/Secret Key pairs, defining permission scopes, implementing signature generation and verification, employing timestamps and nonces to prevent replay attacks, and outlining practical API endpoint specifications and response standards.

Top Architect
Top Architect
Top Architect
Designing Secure Third‑Party API Authentication with AK/SK, Signatures and Token Mechanisms

Design Overview

When exposing APIs to third‑party systems, data integrity, freshness, and idempotency must be ensured. The proposed solution uses API keys (Access Key/Secret Key) for authentication and a callback URL for asynchronous notifications.

Permission Division

Each application is identified by appId . An appKey (public identifier) and appSecret (private credential) are generated, allowing multiple appKey+appSecret pairs per appId to achieve fine‑grained permission control.

During authorization, the client sends appKey and appSecret to the server.

The server validates them against stored records and issues a short‑lived token .

Subsequent requests must include the token .

Signature Process

Clients create a signature by concatenating all request parameters (excluding empty values and the signature itself) in alphabetical order, appending the secret key, and computing an MD5 hash (uppercase). The server reproduces the same steps to verify authenticity.

Signature Rules

Generate a unique appId and appSecret for each consumer.

Include a timeStamp (ms) valid for a short window (e.g., 5 minutes) to mitigate replay attacks.

Add a random nonce (≥10 characters) to guarantee request uniqueness.

Store used nonces in Redis with a TTL matching the timestamp window; reject repeated nonces.

Combine appId , timeStamp , nonce , and other parameters, then compute the MD5 signature.

API Interface Design

Typical CRUD endpoints are defined as follows (all using POST for better security):

GET /api/resources – list resources (optional page , limit ).

POST /api/resources – create a resource (required name ).

PUT /api/resources/{resourceId} – update a resource.

DELETE /api/resources/{resourceId} – delete a resource.

Responses follow a unified format containing code , message , and data .

Security Considerations

Enforce HTTPS for all communications.

Validate signatures and timestamps on the server side.

Encrypt sensitive data in transit using TLS.

AK/SK Generation Scheme

Implement an API key management system that creates a unique access_key (public) and secret_key (private) for each client, stores them securely (e.g., encrypted in a database), and provides a secure distribution channel.

CREATE TABLE api_credentials (
    id INT AUTO_INCREMENT PRIMARY KEY,
    app_id VARCHAR(255) NOT NULL,
    access_key VARCHAR(255) NOT NULL,
    secret_key VARCHAR(255) NOT NULL,
    valid_from DATETIME NOT NULL,
    valid_to DATETIME NOT NULL,
    enabled TINYINT(1) NOT NULL DEFAULT 1,
    allowed_endpoints VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Additional API Design Details

All endpoints use POST to avoid exposing parameters in URLs.

IP whitelist can be enforced via firewall or Sentinel.

Rate limiting per IP + endpoint using Redis counters.

Request logging via AOP for troubleshooting.

Sensitive fields are encrypted (e.g., RSA) before storage.

Idempotency is achieved by client‑generated unique IDs stored in Redis.

Versioning is handled by URL prefixes such as /v1/... or /v2/... .

Standard HTTP status codes are used (200 success, 4xx client error, 5xx server error).

public enum CodeEnum {
    SUCCESS(200, "处理成功"),
    ERROR_PATH(404, "请求地址错误"),
    ERROR_SERVER(505, "服务器内部发生错误");
    private int code;
    private String message;
    CodeEnum(int code, String message) { this.code = code; this.message = message; }
    public int getCode() { return code; }
    public String getMessage() { return message; }
}
public class Result implements Serializable {
    private int code;
    private String message;
    private Object data = null;
    // getters and setters omitted for brevity
    public Result fillCode(CodeEnum e) { this.code = e.getCode(); this.message = e.getMessage(); return this; }
    public Result fillData(Object d) { this.code = CodeEnum.SUCCESS.getCode(); this.message = CodeEnum.SUCCESS.getMessage(); this.data = d; return this; }
}

Signature Interceptor Example (Spring MVC)

public class SignAuthInterceptor implements HandlerInterceptor {
    private RedisTemplate
redisTemplate;
    private String key;
    public SignAuthInterceptor(RedisTemplate
redisTemplate, String key) {
        this.redisTemplate = redisTemplate;
        this.key = key;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String timestamp = request.getHeader("timestamp");
        String nonceStr = request.getHeader("nonceStr");
        String signature = request.getHeader("signature");
        long NONCE_TIMEOUT = 60L;
        if (StrUtil.isEmpty(timestamp) || DateUtil.between(DateUtil.date(Long.parseLong(timestamp) * 1000), DateUtil.date(), DateUnit.SECOND) > NONCE_TIMEOUT) {
            throw new BusinessException("invalid timestamp");
        }
        Boolean haveNonce = redisTemplate.hasKey(nonceStr);
        if (StrUtil.isEmpty(nonceStr) || haveNonce == null || haveNonce) {
            throw new BusinessException("invalid nonceStr");
        }
        if (StrUtil.isEmpty(signature) || !Objects.equals(signature, this.signature(timestamp, nonceStr, request))) {
            throw new BusinessException("invalid signature");
        }
        redisTemplate.opsForValue().set(nonceStr, nonceStr, NONCE_TIMEOUT, TimeUnit.SECONDS);
        return true;
    }
    private String signature(String timestamp, String nonceStr, HttpServletRequest request) throws UnsupportedEncodingException {
        Map
params = new HashMap<>(16);
        Enumeration
en = request.getParameterNames();
        while (en.hasMoreElements()) {
            String name = en.nextElement();
            String value = request.getParameter(name);
            params.put(name, URLEncoder.encode(value, "UTF-8"));
        }
        String qs = String.format("%s×tamp=%s&nonceStr=%s&key=%s", this.sortQueryParamString(params), timestamp, nonceStr, key);
        return SecureUtil.md5(qs).toLowerCase();
    }
    private String sortQueryParamString(Map
params) {
        List
keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);
        StringBuilder sb = new StringBuilder();
        for (String k : keys) { sb.append(k).append("=").append(params.get(k)).append("&"); }
        return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : sb.toString();
    }
}

Token Generation and Usage

Tokens (access tokens) are UUIDs stored in Redis with associated user or application data. Clients obtain a token after successful login or API‑key exchange, then include the token in every request header. The server validates token existence and expiry before processing the request.

Conclusion

The presented design combines API key management, request signing, timestamp/nonce replay protection, token‑based authentication, and standardized response structures to build a robust, secure third‑party API ecosystem.

JavaAccess ControlauthenticationtokenAPI securitySignature
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.