Securing OpenAPI Interfaces with AppId, AppSecret, and RSA Signatures
This article explains how to standardize and protect OpenAPI interfaces by using globally unique AppId/AppSecret pairs, generating RSA‑based signatures, implementing timestamp and nonce checks, and applying common security measures such as rate limiting, whitelist/blacklist controls, and sensitive data handling, all illustrated with complete Java code examples.
Introduction
To ensure standardized, reusable, and secure software interfaces, the OpenAPI specification defines a unified protocol for request formats, parameters, responses, and usage, improving maintainability, extensibility, and safety.
1. AppId and AppSecret
AppId Usage
AppId is a globally unique identifier that helps identify users and support data analysis. It is paired with an AppSecret (acting like a password) to prevent malicious use.
AppId and AppSecret are combined to create a signature that is encrypted according to a specific rule. The client includes this signature in the request; the server verifies it before allowing data exchange.
AppId Generation
AppId must be globally unique; any method that guarantees uniqueness is acceptable.
AppSecret Generation
AppSecret is treated as a password and should be generated following standard password‑security guidelines.
2. sign Signature
RSASignature
Before discussing signatures, understand two concepts: asymmetric encryption (e.g., RSA) and hash algorithms (e.g., MD5). Asymmetric encryption can be used for public‑key encryption/decryption or, combined with a hash, for digital signatures. RSA signatures typically use SHA256withRSA .
Signature Purpose
Signatures serve two main scenarios: data tampering prevention and identity impersonation protection.
Data Tampering Prevention
By hashing the original data, the server can recompute the hash and compare it with the client’s value; a match confirms the data has not been altered during transmission.
Identity Impersonation Prevention
Using SHA256withRSA , the client hashes the data, encrypts the hash with its private RSA key, and the server verifies the signature with the corresponding public key.
3. Usage Example
Pre‑conditions
When no automated platform exists, AppId and AppSecret are distributed offline; the secret must be stored securely by the client. Public‑private key pairs are also generated offline and shared securely.
Interaction Flow
Client Preparation
1. Compute a SHA‑256 hash of the business parameters to obtain sign :
<code>// Business request parameters
UserEntity userEntity = new UserEntity();
userEntity.setUserId("1");
userEntity.setPhone("13912345678");
// Generate signature using SHA‑256
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
</code>2. Assemble header parameters (appId, nonce, sign, timestamp) in natural order, filter out empty values, and sign them with the private key to obtain appSign :
<code>Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("appSecret=").append(appSecret);
System.out.println("[Client] Concatenated parameters: " + sb.toString());
String appSign = sha256withRSASignature(appKeyPair.get(appId).get("privateKey"), sb.toString());
System.out.println("[Client] appSign: " + appSign);
</code>3. Build the request object and send it to the server:
<code>Header header = Header.builder()
.appId(appId)
.nonce(nonce)
.sign(sign)
.timestamp(timestamp)
.appSign(appSign)
.build();
APIRequestEntity apiRequestEntity = new APIRequestEntity();
apiRequestEntity.setHeader(header);
apiRequestEntity.setBody(userEntity);
String requestParam = JSONObject.toJSONString(apiRequestEntity);
System.out.println("[Client] Request parameters: " + requestParam);
</code>Server Preparation
1. Parse the request, recompute the sign , and compare it with the header value:
<code>Header header = apiRequestEntity.getHeader();
UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
if (!sign.equals(header.getSign())) {
throw new Exception("Data signature error!");
}
</code>2. Retrieve appSecret using appId , rebuild the signature string, and verify it with the public key:
<code>String appId = header.getAppId();
String appSecret = getAppSecret(appId);
String nonce = header.getNonce();
String timestamp = header.getTimestamp();
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[0]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("appSecret=").append(appSecret);
if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {
throw new Exception("Public key verification error!");
}
System.out.println("[Server] Verification passed!");
</code>4. Common Protection Measures
Timestamp
Timestamp limits request validity to a configurable window (e.g., 5 minutes). The server checks the difference between the current time and the timestamp; requests older than the window are rejected.
<code>long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if ((now - Long.parseLong(timestamp)) / 1000 / 60 >= 5) {
throw new Exception("Request expired!");
}
</code>Nonce
Nonce is a random value generated per request. The server stores each nonce (e.g., in a Guava cache with a 5‑minute TTL). If a nonce is seen again within the TTL, the request is considered a replay and rejected.
<code>String cacheKey = appId + "_" + nonce;
if (cache.getIfPresent(cacheKey) != null) {
throw new Exception("Request invalid – replay detected!");
}
cache.put(cacheKey, "1");
</code>Access Permissions
Data access should be scoped to the AppId’s authorized resources, ensuring each AppId can only retrieve data within its permission set.
Parameter Validation
All incoming parameters must be validated for length, type, format, and business rules. Spring Boot Validation annotations (e.g., @NotBlank , @DecimalMin , @Email ) provide a concise way to enforce these checks.
<code>@NotBlank(message = "Name cannot be empty")
private String name;
@DecimalMin(value = "5", message = "Amount must be >= 5")
@DecimalMax(value = "10", message = "Amount must be <= 10")
private BigDecimal amount;
</code>Rate Limiting
To protect services from overload or abuse, apply rate limiting (e.g., Guava RateLimiter for single‑node, Redis or Sentinel for distributed environments).
Sensitive Data Handling
Sensitive fields such as ID numbers, phone numbers, or bank cards must be masked or encrypted before storage or transmission.
Whitelist / Blacklist
Whitelist IPs for trusted service‑to‑service calls; blacklist IPs to block known malicious sources.
5. Additional Considerations
API name and description should be concise and clearly convey purpose.
Support standard HTTP methods (GET, POST, PUT, DELETE) with well‑defined request/response schemas.
Define comprehensive error codes and messages.
Provide thorough documentation and sample data for developers.
Design APIs to be extensible (e.g., versioning) while maintaining backward compatibility.
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.