Information Security 31 min read

Design and Implementation of Secure Third‑Party API Authentication Using Access Key/Secret Key, Signatures, and Tokens

This article presents a comprehensive design for securing third‑party API calls by generating unique Access Key/Secret Key pairs, implementing request signing with timestamps and nonces, managing token‑based authentication, defining permission granularity, and providing concrete Java and SQL code examples along with best‑practice recommendations for HTTPS, rate limiting, logging, and idempotency.

Architect
Architect
Architect
Design and Implementation of Secure Third‑Party API Authentication Using Access Key/Secret Key, Signatures, and Tokens

When exposing third‑party interfaces, data integrity, freshness, and replay protection must be considered; the article outlines a security‑first design that uses API keys (Access Key/Secret Key) for authentication and callback URLs for asynchronous notifications.

Design Overview

1. API Key Generation : Each third‑party application receives a unique Access Key (AK) for identification and a Secret Key (SK) for signing and encryption.

AK: Access Key Id, used to identify the user. SK: Secret Access Key, used to generate and verify signatures and must be kept confidential.

2. Interface Authentication : Clients sign request parameters with AK and SK, placing the signature in headers or query parameters.

3. Callback URL : Third‑party apps provide a callback URL to receive asynchronous results.

4. API Definition : Define URL, HTTP method, request parameters, and response format.

Permission Division

Key concepts:

appID : Unique application identifier.

appKey : Public key (account) used in requests.

appSecret : Private key (password) used for signing.

token : Time‑limited access token.

Usage flow:

Request authorization with AppKey and AppSecret (stored server‑side).

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

Subsequent client requests must include the token .

Signature Process

The request includes timestamp (ms), nonce (random string), and sign . The server checks that the timestamp is within an acceptable window (e.g., 60 seconds) and that the nonce has not been used before (stored in Redis).

If validation fails, the request is rejected as a replay attack.

Signature Rules

Assign appId and appSecret to each caller.

Add timeStamp (server time, ms) – limits request validity.

Add nonce (≥10 characters) – prevents duplicate submissions.

Compute sign using all parameters (excluding empty values) sorted alphabetically, concatenated as key1value1key2value2… , append the secret, then MD5‑hash and uppercase the result.

API Interface Design

Example CRUD endpoints for a generic /api/resources resource, using POST for all operations to avoid exposing parameters in URLs.

GET list – pagination parameters page , limit .

POST create – required name , optional description .

PUT update – resourceId path param, optional name , description .

DELETE delete – resourceId path param.

Security Considerations

Use HTTPS for encrypted transport.

Validate signatures and timestamps on each request.

Encrypt sensitive data with TLS.

Code Examples

Java interceptor that validates timestamp, nonce, and signature, storing used nonces in Redis:

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 {
        // Get timestamp, nonce, signature
        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 setup for HTTPS connections in Java:

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

SQL schema for storing API credentials:

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

Enum for unified response codes:

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

Standard API response wrapper:

public class Result implements Serializable {
    private int code;
    private String message;
    private Object data = null;
    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;
    }
    // getters and setters omitted for brevity
}

Token Management

Tokens (access tokens) are UUIDs stored in Redis with an expiration time; they are issued after a successful login (appId + appSecret) and must be presented with each subsequent request.

Two token types are described:

API Token : Used for unauthenticated endpoints (login, registration).

User Token : Used for endpoints requiring a logged‑in user.

Combining token authentication with request signing (appSecret + parameters) mitigates token theft and request tampering.

Additional Best Practices

Rate limiting per IP using Redis keys.

Logging each request for troubleshooting.

Data masking/encryption for sensitive fields (e.g., RSA).

Idempotency via client‑generated unique IDs stored in Redis.

API versioning in URLs (e.g., /v1/list ).

Consistent response format: {code, message, data} .

Swagger/OpenAPI documentation for discoverability.

backendtokenAPI securitySignatureaccess key
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.