AES 加密與失敗處理
概述
CDR 推送支援 AES 加密和簽名驗證機制,確保數據傳輸的安全性和完整性。同時實現了智能重試機制,確保 CDR 數據的可靠交付。
AES 加密機制
加密概述
- 演算法:AES-128-CBC
- 密鑰長度:128 bit (16 字節)
- 初始化向量 (IV):128 bit (16 字節),通常為隨機生成
- 填充方式:PKCS7
加密流程
步驟 1:準備數據
原始 CDR JSON 數據:
json
{
"call": {
"id": "call_20260323_001",
"callType": 2,
"duration": 150,
...
},
...
}步驟 2:生成 IV
隨機生成 16 字節的初始化向量:
IV = random_bytes(16)步驟 3:AES 加密
使用提供的密鑰和 IV 對 JSON 字符串進行 AES-128-CBC 加密:
encrypted_data = AES_Encrypt(json_string, key, iv)步驟 4:組裝加密包
encrypted_payload = base64(iv) + ":" + base64(encrypted_data)HTTP 推送格式
加密後的 CDR 通過以下格式推送:
http
POST {webhookUrl}
Content-Type: application/json
X-AICC-Signature: {signature}
X-AICC-Timestamp: {timestamp}請求體:
json
{
"encrypted": true,
"encryptionType": "AES-128-CBC",
"payload": "{base64_iv}:{base64_encrypted_data}"
}Python 解密示例
python
import base64
from Crypto.Cipher import AES
import json
def decrypt_cdr(encrypted_payload, key):
"""
解密 CDR 數據
Args:
encrypted_payload: 格式為 "base64_iv:base64_encrypted_data" 的字符串
key: AES 密鑰 (16 字節)
Returns:
解密後的 CDR 字典
"""
# 分割 IV 和加密數據
parts = encrypted_payload.split(':')
if len(parts) != 2:
raise ValueError("Invalid encrypted payload format")
iv = base64.b64decode(parts[0])
encrypted_data = base64.b64decode(parts[1])
# 解密
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_data)
# 移除 PKCS7 填充
padding_length = decrypted[-1]
decrypted = decrypted[:-padding_length]
# 解析 JSON
cdr_data = json.loads(decrypted.decode('utf-8'))
return cdr_data
# 使用示例
cdr_key = b'your_aes_key_16bytes' # 16 字節的密鑰
@app.route('/cdr/webhook', methods=['POST'])
def handle_cdr():
data = request.get_json()
if data.get('encrypted'):
payload = data.get('payload')
cdr_data = decrypt_cdr(payload, cdr_key)
else:
cdr_data = data
# 處理 CDR 數據
print(cdr_data)
return {'code': 0, 'message': 'success'}Node.js 解密示例
javascript
const crypto = require('crypto');
function decryptCdr(encryptedPayload, key) {
// 分割 IV 和加密數據
const parts = encryptedPayload.split(':');
if (parts.length !== 2) {
throw new Error('Invalid encrypted payload format');
}
const iv = Buffer.from(parts[0], 'base64');
const encrypted = Buffer.from(parts[1], 'base64');
// 解密
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
// 移除 PKCS7 填充
const paddingLength = decrypted[decrypted.length - 1];
decrypted = decrypted.slice(0, decrypted.length - paddingLength);
// 解析 JSON
const cdrData = JSON.parse(decrypted.toString('utf-8'));
return cdrData;
}
// 使用示例
app.post('/cdr/webhook', (req, res) => {
try {
if (req.body.encrypted) {
const key = Buffer.from('your_aes_key_16bytes');
const cdrData = decryptCdr(req.body.payload, key);
console.log(cdrData);
} else {
console.log(req.body);
}
res.json({ code: 0, message: 'success' });
} catch (error) {
console.error('Decryption error:', error);
res.status(500).json({ code: 500, message: 'error' });
}
});Java 解密示例
java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import org.json.JSONObject;
public class CDRDecryptor {
public static JSONObject decryptCdr(String encryptedPayload, byte[] key)
throws Exception {
// 分割 IV 和加密數據
String[] parts = encryptedPayload.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid encrypted payload format");
}
byte[] iv = Base64.getDecoder().decode(parts[0]);
byte[] encrypted = Base64.getDecoder().decode(parts[1]);
// 初始化 AES Cipher
SecretKeySpec keySpec = new SecretKeySpec(key, 0, key.length, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 解密
byte[] decrypted = cipher.doFinal(encrypted);
String jsonString = new String(decrypted, "UTF-8");
// 解析 JSON
return new JSONObject(jsonString);
}
// 使用示例
public static void main(String[] args) throws Exception {
byte[] key = "your_aes_key_16bytes".getBytes();
String encryptedPayload = "...";
JSONObject cdrData = decryptCdr(encryptedPayload, key);
System.out.println(cdrData.toString());
}
}簽名驗證
簽名算法
簽名用於驗證 CDR 推送的完整性和來源,防止數據被篡改。
signature = SHA256(payload + secretKey + timestamp)其中:
payload是 HTTP 請求體的完整內容 (JSON 字符串)secretKey是配置的 Webhook 密鑰timestamp是請求時間戳
Python 簽名驗證
python
import hashlib
import json
def verify_signature(raw_body, signature, timestamp, secret_key):
"""驗證 CDR 推送簽名"""
message = raw_body + secret_key + timestamp
expected_signature = hashlib.sha256(message.encode()).hexdigest()
return signature == expected_signature
@app.route('/cdr/webhook', methods=['POST'])
def handle_cdr():
signature = request.headers.get('X-AICC-Signature')
timestamp = request.headers.get('X-AICC-Timestamp')
raw_body = request.get_data(as_text=True)
secret_key = "your_webhook_secret_key"
if not verify_signature(raw_body, signature, timestamp, secret_key):
return {'code': 401, 'message': 'Invalid signature'}, 401
# 簽名驗證成功,處理 CDR
return {'code': 0, 'message': 'success'}CDR 推送失敗重試機制
重試策略
當業務系統推送 CDR 失敗時,AICC 系統會自動按照以下策略進行重試:
| 重試次數 | 延遲時間 | 累計延遲 | 說明 |
|---|---|---|---|
| 1 | 立即 | 0 秒 | 首次發送 |
| 2 | 2 秒 | 2 秒 | 第 1 次重試 |
| 3 | 4 秒 | 6 秒 | 第 2 次重試 |
| 4 | 8 秒 | 14 秒 | 第 3 次重試 |
| 5 | 16 秒 | 30 秒 | 第 4 次重試 |
| 6 | 32 秒 | 62 秒 | 第 5 次重試 |
| 7 | 64 秒 | 126 秒 | 第 6 次重試 |
| 8 | 128 秒 | 254 秒 | 第 7 次重試 |
| 9 | 256 秒 | 510 秒 | 第 8 次重試 |
| 10 | 512 秒 | 1022 秒 | 第 9 次重試 |
重試停止條件
重試會在以下任何條件滿足時停止:
- 成功推送:收到 HTTP 200-299 狀態碼且回應
code: 0 - 達到最大重試次數:共進行 10 次嘗試 (1 次初始 + 9 次重試)
- 永久錯誤:收到 4xx 狀態碼 (除 408, 429 外),表示業務邏輯錯誤,不重試
重試失敗處理
當所有重試都失敗後:
- 記錄失敗日誌:系統記錄失敗的 CDR 及原因
- 告警通知:向管理員發送告警通知
- 人工介入:管理員可通過後台重新手動觸發推送
- 數據保留:失敗的 CDR 數據保留 30 天,可通過後台查詢
重試失敗原因分析
常見的推送失敗原因及解決方案:
| 原因 | 狀態碼 | 說明 | 解決方案 |
|---|---|---|---|
| 連接超時 | 連接失敗 | 網絡問題或服務器無響應 | 檢查網絡連接和服務器狀態 |
| 請求超時 | 408 | 請求超時 (將重試) | 優化後端處理性能 |
| 服務不可用 | 503 | 服務器維護或過載 (將重試) | 等待服務器恢復 |
| 非法簽名 | 401 | 簽名驗證失敗 | 檢查密鑰配置是否正確 |
| 請求錯誤 | 400 | 請求格式或內容錯誤 | 檢查 CDR 數據格式 |
| 限流 | 429 | 請求過於頻繁 (將重試) | 檢查 Webhook 配置的速率限制 |
| 內部錯誤 | 500 | 業務系統內部錯誤 (不重試) | 檢查業務系統日誌 |
CDR 推送配置
Webhook 配置參數
json
{
"webhookUrl": "https://api.example.com/cdr/webhook",
"secretKey": "your_webhook_secret_key",
"encryptionKey": "your_aes_key_16bytes",
"encryptionEnabled": true,
"maxRetries": 10,
"retryInterval": "exponential",
"enabled": true
}配置說明
| 參數名 | 類型 | 必填 | 說明 |
|---|---|---|---|
webhookUrl | string | 是 | CDR 推送地址 (必須 HTTPS) |
secretKey | string | 是 | 用於簽名驗證的密鑰 |
encryptionKey | string | 否 | AES 加密密鑰 (16 字節) |
encryptionEnabled | boolean | 否 | 是否啟用加密,預設為 false |
maxRetries | integer | 否 | 最大重試次數,預設為 10 |
retryInterval | string | 否 | 重試間隔策略 (exponential 指數級,預設值) |
enabled | boolean | 否 | 是否啟用此 Webhook,預設為 true |
最佳實踐
安全建議
- HTTPS 加密傳輸:所有 Webhook URL 必須使用 HTTPS
- 密鑰管理:定期更換 secretKey 和 encryptionKey,避免洩露
- 簽名驗證:始終驗證 X-AICC-Signature,防止非法請求
- 速率限制:根據服務能力配置合理的 Webhook 速率限制
- 日誌記錄:記錄所有接收的 CDR,便於問題追蹤
性能建議
- 異步處理:CDR 接收後應放入隊列,異步處理避免阻塞
- 快速應答:快速返回 200 OK,不要在 Webhook 中進行耗時操作
- 冪等設計:實現冪等性,重複接收同一 CDR 應返回相同結果
- 數據校驗:驗證 CDR 數據完整性,檢查必填字段
監控告警
- 推送失敗監控:監控 Webhook 推送失敗率
- 延遲監控:監控 CDR 推送到接收的延遲時間
- 告警規則:當失敗率超過閾值時發出告警
- 日誌分析:定期分析失敗日誌,發現問題根源
常見問題
Q1: 如何修改加密密鑰?
修改後的推送將使用新密鑰加密。建議先部署能支持新密鑰解密的代碼,再修改配置。
Q2: 什麼情況下不會重試?
HTTP 500、502 等伺服器錯誤不會重試 (視為非暫時性錯誤)。
Q3: 如何查看推送失敗的 CDR?
可在 AICC 後台「系統設定」>「Webhook」>「失敗日誌」中查看。
Q4: 是否支援自定義重試策略?
目前不支持自定義重試策略,使用固定的指數級退避策略。