Secure Third‑Party API Design: AK/SK, Token, Signature, Timestamp & Nonce
The article presents a comprehensive guide to designing secure third‑party APIs, covering access‑key/secret‑key generation, token management, signature algorithms, timestamp and nonce anti‑replay mechanisms, permission granularity, request logging, rate limiting, and example Java and SQL implementations.
When providing interfaces to third‑party systems, data security concerns such as tampering, staleness, and duplicate submissions must be addressed.
The design proposes using an Access Key/Secret Key pair for authentication, generating a unique API key pair (AK for identification, SK for signing) for each third‑party application.
AK: Access Key Id, used to identify the user. SK: Secret Access Key, used for signing and must be kept confidential.
Authentication flow: the client signs request parameters with AK and SK, includes the signature in headers or parameters, and the server validates the signature.
Permission division is achieved by combining appId , appKey , and appSecret . Multiple appKey+appSecret pairs can be created to assign different read/write permissions to different developers.
Two simplified scenarios are described:
Open APIs where appId = appKey = appSecret and only the appId is needed for usage statistics.
Closed APIs where each user has a unique appId+appSecret pair.
To prevent replay attacks, each request must carry a timestamp (valid for a short window, e.g., 60 seconds) and a nonce (a unique random string). The server stores used nonces in Redis with an expiration equal to the timestamp window.
Signature generation steps:
Collect all non‑empty parameters except sign and sort them alphabetically.
Concatenate key1value1key2value2… into a string.
Append the secret key.
Compute the MD5 hash of the resulting string and convert it to uppercase; this is the sign .
Example Java interceptor that validates timestamp, nonce, and signature:
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();
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;
}
/** Sort parameters alphabetically */
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();
}
}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
);Additional best practices include using HTTPS, IP white‑listing, rate limiting via Redis, logging, data masking, idempotency handling with unique request IDs, versioning of APIs, standardized response structures, and Swagger documentation.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.