Designing Elegant and Secure Third‑Party APIs: Key Practices and Pitfalls
The article presents a comprehensive design for third‑party APIs that protects against data tampering, replay attacks, and unauthorized access by using AK/SK key pairs, timestamp and nonce signatures, TLS encryption, fine‑grained permission models, rate limiting, idempotency handling, versioning, standardized response formats, and detailed implementation examples in Java.
Design Overview
When exposing an interface to third‑party systems, the design must guarantee data integrity, freshness and replay protection. The solution uses an Access Key/Secret Key (AK/SK) pair for authentication and a callback URL for asynchronous notifications.
API Key Generation
For each third‑party application generate a unique AK (identifies the app) and SK (used for signing and must be kept secret).
AK: Access Key Id, used to identify the user. SK: Secret Access Key, used to encrypt authentication strings and verify signatures; it must remain confidential.
Permission Partitioning
appId is the unique identifier of the application (user ID) and can map to multiple appKey+appSecret pairs for fine‑grained permission control.
appKey is the public identifier required to call services.
appSecret is the private key paired with appKey.
token is a short‑lived credential issued after the initial appKey/appSecret exchange.
Client sends appKey and appSecret to the third‑party server.
Server validates the pair against its database or cache.
If valid, a unique token is generated and returned.
Subsequent requests must include the token.
The appKey+appSecret pair enables initial authentication (similar to login) and token issuance, allowing different permissions for the same appId.
When a single appId needs multiple permission sets (e.g., read‑only vs read‑write), multiple appKey+appSecret pairs can be created and associated with distinct privileges.
Simplified Scenarios
Scenario 1 : Open APIs (e.g., map services) may treat appId, appKey and appSecret as the same value, using it only for usage statistics.
Scenario 2 : When each user has a single permission set, appKey can be omitted and appId equals appKey, with a unique appSecret per user.
Signature Process
The client builds a signature from appKey, timestamp, nonce and appSecret (using SHA‑1 or MD5). The server recomputes the signature and compares it.
Signature Rules
Assign appId and appSecret to different callers; obtain them via an online portal or offline issuance.
Include a timestamp (milliseconds) that is valid for 5 minutes to mitigate DoS and replay attacks.
Include a random nonce (≥10 characters) that must be unique within its validity window to prevent duplicate submissions.
Include a sign field calculated from the sorted request parameters, the timestamp, the nonce and the secret key.
The combination timestamp+nonce prevents replay attacks; the server stores used nonces in Redis with a 60‑second TTL and rejects repeats.
All authentication fields are transmitted in request headers.
API Design Examples
GET /api/resources – list resources (optional page, limit).
POST /api/resources – create a resource (required name, optional description).
PUT /api/resources/{resourceId} – update a resource.
DELETE /api/resources/{resourceId} – delete a resource.
Responses use standard HTTP status codes (200, 201, 204) and JSON bodies.
Security Considerations
Use HTTPS for all transport.
Validate AK and signature on every request.
Encrypt sensitive data with TLS.
Replay‑Attack Prevention
Attach timestamp and nonce to each request; the server checks that the timestamp is within an acceptable window (e.g., 60 seconds) and that the nonce has not been seen before.
public class SignAuthInterceptor implements HandlerInterceptor {
private RedisTemplate<String, String> redisTemplate;
private String key;
public SignAuthInterceptor(RedisTemplate<String, String> 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<String, Object> params = new HashMap<>(16);
Enumeration<String> enumeration = request.getParameterNames();
while (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);
String sign = SecureUtil.md5(qs).toLowerCase();
return sign;
}
private String sortQueryParamString(Map<String, Object> params) {
List<String> 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("&");
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
}TLS Encryption 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 conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());AK/SK Management
Typical steps (inspired by major cloud providers): create a credential management service, generate random AK (public) and secure random SK, store them in a database, protect SK with encryption, provide a self‑service portal for clients, and rotate keys periodically.
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 Recommendations
Use POST for all operations to avoid exposing parameters in URLs.
Apply IP whitelist or firewall rules to restrict access.
Implement per‑IP rate limiting with Redis (key = ip+endpoint, value = request count, TTL = window).
Log every request (e.g., via AOP) for troubleshooting.
Mask sensitive fields using RSA asymmetric encryption.
Ensure idempotency: generate a globally unique nonce for write operations, store the result keyed by the nonce in Redis with an expiration, and reject duplicate submissions.
Version APIs (e.g., /v1/…, /v2/…) and never change a released contract without a new version.
Standardize response format with fields code, message, and data.
public class Result implements Serializable {
private int code;
private String message;
private Object data = null;
// getters, setters omitted
}Provide Swagger or similar documentation for developers.
Signature Generation Steps
Collect all request parameters (including appId, timestamp, nonce), exclude the sign itself and any empty values.
Sort parameters alphabetically and concatenate as key1value1key2value2….
Append the secret key to the concatenated string.
Compute the MD5 hash of the result, convert to uppercase; this is the sign value.
Example: for request
appId=zs001&timeStamp=1612691221000&nonce=1234567890&k1=v1&k2=v2&method=cancelwith secret miyao, the final string is
appIdzs001k1v1k2v2methodcancelnonce1234567890timeStamp1612691221000miyao, MD5 → ABCDEF (uppercase) used as sign.
Token Concepts
A token (access token) represents the caller’s identity after the initial appKey/appSecret exchange. Tokens are typically UUIDs stored in Redis; their presence validates the request. Two token types exist: API Token: for unauthenticated endpoints (login, public data); obtained with appId, timestamp and sign. USER Token: for user‑specific endpoints; obtained after user login with credentials.
Combining token validation with signature verification ensures that even if a token is stolen, an attacker cannot forge requests without the secret key.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
