How to Secure Third‑Party APIs with AK/SK, Tokens, and Signature Strategies
This article walks through a complete security design for third‑party APIs, covering API‑key generation, request signing with timestamps and nonces, token handling, permission granularity, database schema, and practical implementation details such as rate limiting, idempotency, and TLS encryption.
Design Overview
When exposing an API to third‑party systems, the primary security concerns are data tampering, stale data, and duplicate submissions. 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 client a unique AK (public identifier) and SK (private secret) are generated. AK identifies the caller; SK signs and encrypts request payloads.
AK : Access Key Id, used to label the user. SK : Secret Access Key, kept confidential and used to generate and verify signatures.
Permission Model
Three identifiers are introduced:
appID : unique application identifier, indexed for fast lookup.
appKey : public key (account) used in API calls.
appSecret : private key (password) paired with appKey for signing.
token : time‑limited access token.
Multiple appKey/appSecret pairs can be attached to a single appID to represent different permission sets. Example: one pair grants delete‑only permission, another grants read‑write permission. Database indexes on appID enable fast permission lookup.
Simplified Scenarios
Open APIs (e.g., map services) collapse the three identifiers into one appId = appKey = appSecret. The identifier is used only for usage statistics.
When each user has a single permission set, app_id = app_key and a single appId+appSecret pair suffices.
Signature Process
The request validation flow consists of the following steps:
Assign a globally unique appId and a secret appSecret to each caller.
Add a timestamp (server time in ms) that is valid for 5 minutes. Requests whose timestamp differs from the server time by more than the threshold are rejected.
Add a nonce (at least 10 characters) to guarantee uniqueness within the validity window.
Validate the nonce by checking a Redis key nonce:{nonce}. If the key exists, the request is a replay; otherwise store the key with the same TTL as the timestamp.
Compute a sign field from timestamp, nonce, and all other parameters (excluding empty values and sign itself). The server recomputes the signature using the shared secret and compares it to the received value.
Below is a concrete Java HandlerInterceptor implementation that enforces the above checks, stores the nonce in Redis for 60 seconds, and throws business exceptions on validation failures.
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_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<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);
log.info("qs:{}", qs);
String sign = SecureUtil.md5(qs).toLowerCase();
log.info("sign:{}", sign);
return sign;
}
private String sortQueryParamString(Map<String, Object> params) {
List<String> 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();
}
}API Design Example
A minimal CRUD API for a generic "resource" entity:
GET /api/resources – list resources (optional page and limit).
POST /api/resources – create a resource ( name required, description optional).
PUT /api/resources/{resourceId} – update a resource.
DELETE /api/resources/{resourceId} – delete a resource.
Additional Security Measures
Enforce HTTPS for all traffic.
Validate AK and sign on every request.
Encrypt sensitive data with TLS.
Replay‑Attack Prevention
A timestamp alone leaves a 60‑second replay window. Adding a nonce eliminates replay within that window. The server stores each nonce in Redis with a short TTL; duplicate nonces trigger replay detection.
Nonce Management
To avoid unbounded growth of stored nonces, the timestamp can be used as a cleanup trigger while the nonce guarantees uniqueness within the timestamp window.
Token Generation and Usage
After a successful login, a UUID token is stored in Redis with an expiration. Subsequent requests include the token instead of appId. Two token types are defined:
API Token : obtained with appId, timestamp, and sign; used for public endpoints (login, registration).
User Token : obtained with username/password; used for authenticated user actions.
Combining a token with the signature mechanism prevents token theft from being sufficient for request forgery because the attacker still lacks the secret key needed to compute a valid sign.
AK/SK Management Strategy
Following cloud‑provider best practices, the lifecycle consists of:
Create a service that generates a random AK (public) and a random, securely stored SK (private) for each client.
Persist the pair in a database table (see SQL below).
Expose an API or UI for clients to retrieve their credentials over a secure channel.
Rotate keys periodically and enforce strict access controls.
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
);Supplementary API Recommendations
Use POST for all API calls to avoid exposing parameters in URLs.
Configure IP whitelists or firewall rules to restrict access.
Implement per‑IP rate limiting with Redis counters.
Log every request for troubleshooting.
Mask sensitive fields (e.g., order numbers) using RSA asymmetric encryption.
Ensure idempotency by generating a globally unique request identifier and storing the result in Redis.
Version APIs in the URL (e.g., /v1/list, /v2/list).
Standardize response format with code, message, and data fields; provide an enum for common status codes.
public enum CodeEnum {
SUCCESS(200, "Success"),
ERROR_PATH(404, "Invalid path"),
ERROR_SERVER(505, "Server error");
private int code;
private String message;
CodeEnum(int code, String message) { this.code = code; this.message = message; }
// getters omitted
} public class Result implements Serializable {
private int code;
private String message;
private Object data = null;
// getters, setters, and helper methods omitted
}Signature Generation Steps
Collect **all** request parameters, including appId, timestamp, nonce, and any business fields. Exclude the sign parameter itself and any parameters with empty values.
Sort the remaining parameters by key name in ascending ASCII order.
Concatenate each key and value without delimiters, producing a string like key1value1key2value2…keyXvalueX.
Append the secret appSecret (or SK) to the concatenated string.
Compute the MD5 hash of the final string, convert it to uppercase; the result is the sign value.
Example:
Request URL: http://www.example.com/openApi?sign=sign_value&k1=v1&k2=v2&method=cancel&kX=vX Headers: appId:zs001 timestamp:1612691221000 nonce:1234567890
After removing sign and empty parameters, sorting, and concatenating, the string becomes:
appIdzs001k1v1k2v2kXvXmethodcancelnonce1234567890timestamp1612691221000Appending the secret miyao yields:
appIdzs001k1v1k2v2kXvXmethodcancelnonce1234567890timestamp1612691221000miyaoMD5 of the above string (uppercase) is the final sign.
Token Types and Validation
API Token : issued after a successful appId/timestamp/sign exchange; used for endpoints that do not require user login.
User Token : issued after username/password authentication; used for user‑specific actions.
Validation flow:
Client logs in, server returns a UUID token stored in Redis with an expiration.
Client includes the token in the Authorization header of subsequent requests.
Server checks token existence and TTL in Redis; if valid, request proceeds, otherwise it is rejected.
TLS Encryption Example (Java)
// Create SSLContext for TLS
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());
// Open HTTPS connection
URL url = new URL("https://api.example.com/endpoint");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
// Set request parameters, send request, handle responseReferences
For further reading on AK/SK generation patterns see the CSDN article: https://blog.csdn.net/weixin_43460193/article/details/130242478
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.
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.
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.
