Information Security 32 min read

Designing Secure Third‑Party API Authentication with AK/SK, Signatures and Replay‑Attack Prevention

This article presents a comprehensive design for securing third‑party APIs by using Access Key/Secret Key pairs, timestamp and nonce validation, signature generation, token handling, HTTPS, rate limiting, logging, idempotency, versioning, standardized response formats and practical Java code examples to prevent tampering and replay attacks.

Top Architect
Top Architect
Top Architect
Designing Secure Third‑Party API Authentication with AK/SK, Signatures and Replay‑Attack Prevention

Design Overview

When exposing third‑party interfaces, data security concerns such as tampering, staleness and replay attacks must be addressed. The proposed solution uses an Access Key/Secret Key (AK/SK) pair for identity verification and a callback URL for asynchronous notifications.

AK and SK Generation

AK uniquely identifies an application, while SK is a confidential secret used to create signatures. Both are generated per client, stored securely (e.g., encrypted in a database), and can be rotated regularly.

Permission Division

Key concepts include appID (application identifier), appKey (public key), appSecret (private key), and token (temporary credential). The workflow is:

Client sends appKey and appSecret to the server.

Server validates them, generates a unique token , and returns it.

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, and applying MD5 (uppercase). The request also carries a timestamp (ms) and a random nonce (≥10 characters). The server validates:

Timestamp is within an acceptable window (e.g., 60 seconds) to limit replay time.

Nonce has not been used before (stored in Redis with a short TTL).

Signature matches the server‑side calculation.

Implementation Example – Interceptor

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_STR_TIMEOUT_SECONDS = 60L;
        if (StrUtil.isEmpty(timestamp) || DateUtil.between(DateUtil.date(Long.parseLong(timestamp) * 1000), DateUtil.date(), DateUnit.SECOND) > NONCE_STR_TIMEOUT_SECONDS) {
            throw new BusinessException("invalid timestamp");
        }
        Boolean haveNonceStr = redisTemplate.hasKey(nonceStr);
        if (StrUtil.isEmpty(nonceStr) || Objects.isNull(haveNonceStr) || haveNonceStr) {
            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_STR_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        return true;
    }
    private String signature(String timestamp, String nonceStr, HttpServletRequest request) throws UnsupportedEncodingException {
        Map
params = new HashMap<>(16);
        Enumeration
enumeration = request.getParameterNames();
        if (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            String value = request.getParameter(name);
            params.put(name, URLEncoder.encode(value, CommonConstants.UTF_8));
        }
        String qs = String.format("%s×tamp=%s&nonceStr=%s&key=%s", this.sortQueryParamString(params), timestamp, nonceStr, key);
        log.info("qs:{}", qs);
        String sign = SecureUtil.md5(qs).toLowerCase();
        log.info("sign:{}", sign);
        return sign;
    }
    private String sortQueryParamString(Map
params) {
        List
listKeys = Lists.newArrayList(params.keySet());
        Collections.sort(listKeys);
        StrBuilder content = StrBuilder.create();
        for (String param : listKeys) {
            content.append(param).append("=").append(params.get(param).toString()).append("&");
        }
        if (content.length() > 0) {
            return content.subString(0, content.length() - 1);
        }
        return content.toString();
    }
}

TLS Communication Example

SSLContext sslContext = SSLContext.getInstance("TLS");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream("keystore.jks"), "password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
URL url = new URL("https://api.example.com/endpoint");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

Database Schema for Credential Management

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
);

Standardized Response Format

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 codeEnum) {
        this.setCode(codeEnum.getCode());
        this.setMessage(codeEnum.getMessage());
        return this;
    }
    public Result fillData(Object data) {
        this.setCode(CodeEnum.SUCCESS.getCode());
        this.setMessage(CodeEnum.SUCCESS.getMessage());
        this.data = data;
        return this;
    }
}

Best Practices

Enforce HTTPS for all traffic.

Validate AK/SK and signature on every request.

Encrypt sensitive data in transit and at rest.

Apply IP whitelisting, rate limiting (e.g., Redis counters), and detailed request logging.

Implement idempotency tokens for non‑GET operations.

Version APIs and use clear HTTP status codes.

backendauthenticationtokenAPI securitySignatureReplay Attackaccess key
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.