Designing Secure Third‑Party API Authentication with Access Keys, Signatures, and Token Management
This article presents a comprehensive design for securing third‑party API calls by using Access Key/Secret Key pairs, timestamp and nonce based signatures, token generation, permission granularity, TLS encryption, and practical Java code examples to prevent replay and tampering attacks.
Design Overview
When exposing interfaces to third‑party systems, data integrity, freshness, and idempotency must be guaranteed. The proposed solution uses an Access Key/Secret Key pair for authentication, a callback URL for asynchronous notifications, and a detailed permission model.
Permission Division
Each application is identified by an appId . The appKey (public) and appSecret (private) form a pair that can be associated with multiple permissions, enabling fine‑grained access control such as read‑only or read‑write rights.
Signature Process
Requests must include a timeStamp (valid for a short window, e.g., 60 seconds) and a random nonce (at least 10 characters). The server stores used nonces in Redis to reject duplicates, thereby preventing replay attacks.
Signature Rules
Generate a unique appId and secret appSecret for each client.
Append the current timeStamp to the request.
Add a nonce to ensure uniqueness.
Compute a signature sign over all parameters (excluding empty values and the signature itself) sorted alphabetically, concatenate the secret, and apply MD5 (uppercase).
API Interface Design
Typical CRUD endpoints are illustrated:
GET /api/resources – list resources (optional page and limit ).
POST /api/resources – create a resource (requires name ).
PUT /api/resources/{resourceId} – update a resource.
DELETE /api/resources/{resourceId} – delete a resource.
All endpoints should use POST for higher security, enforce IP white‑listing, rate limiting, logging, data masking, idempotency keys, versioning, and a unified response format ( code , message , data ).
Security Considerations
Use HTTPS/TLS for transport encryption, validate appKey / appSecret signatures on every request, and store secrets securely. Implement interceptors to check timestamps, nonces, and signatures before processing the request.
Code Example – Signature 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 {
// Get timestamp, nonce, and signature from headers
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 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 connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
// set other request parameters, send request, handle responseAK/SK Generation and Management
A dedicated credential service creates unique access_key (AK) and secret_key (SK) for each client, stores them in a database with validity periods, and optionally encrypts the SK. Clients obtain their keys via a secure portal or API.
Token and Signature Validation
After login, the server issues a UUID accessToken (token) stored in Redis. Subsequent requests must include the token together with the signed parameters (AK, timestamp, nonce). The interceptor validates token existence, timestamp freshness, nonce uniqueness, and signature correctness before allowing the request.
Additional Best Practices
Use IP white‑listing and per‑IP rate limiting (Redis counters).
Log all API calls for audit and troubleshooting.
Mask sensitive fields (e.g., order numbers) using RSA encryption.
Ensure idempotency by requiring a unique request identifier stored in Redis.
Version APIs (e.g., /v1/ , /v2/ ) and document them with Swagger.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.