1. 支付系统的“隐形护盾”
在构建企业级支付网关或聚合支付系统时,账户合规性校验是保障系统稳定运行的关键一环。对于后端开发者而言,如何在保证数据传输安全(HTTPS + AES)的前提下,将外部的风险监测能力无缝集成到 Spring Boot 项目中,是一个标准的技术课题。
本文将以 天远账户风险监测服务 (JRZQ0B6Y) 为例,从架构设计角度,演示如何编写符合金融级规范的 Java 工具类,实现对 AES-128-CBC 加密签名的封装以及 JSON 数据的强类型映射。
2. 核心技术难点解析
在对接此类金融数据接口时,我们面临两个主要技术要求:
- 传输安全:请求体必须使用 AES-128-CBC 算法加密,并配合随机 IV(初始化向量)防止重放攻击。
- 数据清洗:接口返回的是扁平化的 JSON 数据,需要将其转换为 Java 业务对象(DTO)以便于 Service 层调用。
2.1 技术栈选型
- Framework: Spring Boot 2.x/3.x
- HTTP Client: OkHttp3 (连接池管理更优)
- JSON: Jackson (Spring Boot 默认集成)
- Crypto: JDK 原生
javax.crypto(无需引入 Bouncy Castle 等重型库)
3. 代码实现:AES 加密与 Service 封装
为了保持代码的整洁,我们将加密逻辑与业务逻辑分离。
3.1 AES-128-CBC 工具类
Java 的 PKCS5Padding 实现实际上兼容标准的 PKCS7Padding,因此我们可以直接使用 JDK 原生 API。
Java
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.security.SecureRandom;
public class AesUtil {
/**
* 加密逻辑:随机IV -> AES加密 -> 拼接IV -> Base64编码
*/
public static String encrypt(String content, byte[] keyBytes) throws Exception {
// 1. 生成16字节随机 IV
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
// 2. 拼接 IV + 密文
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
// 3. Base64 编码输出
return Base64.getEncoder().encodeToString(combined);
}
/**
* 解密逻辑:Base64解码 -> 提取IV -> AES解密
*/
public static String decrypt(String base64Data, byte[] keyBytes) throws Exception {
byte[] decoded = Base64.getDecoder().decode(base64Data);
// 1. 提取前16字节 IV
byte[] iv = new byte[16];
System.arraycopy(decoded, 0, iv, 0, 16);
// 2. 提取密文部分
byte[] ciphertext = new byte[decoded.length - 16];
System.arraycopy(decoded, 16, ciphertext, 0, ciphertext.length);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
}
}
3.2 业务 Service 实现
这里演示如何调用 API 并解析返回的“风险标签”。
Java
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class AccountRiskService {
// 替换为实际 API 地址
private static final String API_URL = "https://api.tianyuanapi.com/api/v1/JRZQ0B6Y";
// 建议从 application.yml 读取
private final String accessId = "YOUR_ACCESS_ID";
private final byte[] accessKey = hexToBytes("YOUR_HEX_KEY");
private final OkHttpClient httpClient = new OkHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
public Map<String, Object> checkAccountStatus(String name, String idCard, String mobile, String bankCard) {
try {
// 1. 组装业务参数
Map<String, String> params = new HashMap<>();
params.put("name", name);
params.put("id_card", idCard);
params.put("mobile_no", mobile);
params.put("bank_card", bankCard);
// 2. 加密请求体
String encryptedData = AesUtil.encrypt(objectMapper.writeValueAsString(params), accessKey);
// 3. 发送 POST 请求 (注意必须携带时间戳参数 t)
Map<String, String> bodyMap = new HashMap<>();
bodyMap.put("data", encryptedData);
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
objectMapper.writeValueAsString(bodyMap)
);
Request request = new Request.Builder()
.url(API_URL + "?t=" + System.currentTimeMillis())
.addHeader("Access-Id", this.accessId)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
Map resMap = objectMapper.readValue(response.body().string(), Map.class);
if ((int)resMap.get("code") == 0) {
// 4. 解密响应数据
String decryptedJson = AesUtil.decrypt((String) resMap.get("data"), accessKey);
return objectMapper.readValue(decryptedJson, Map.class);
}
}
}
} catch (Exception e) {
e.printStackTrace(); // 生产环境建议使用 Slf4j
}
return null;
}
// 辅助工具:Hex字符串转Byte数组
private byte[] hexToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
}
return data;
}
}
4. 业务数据模型设计
接口返回的字段虽然都是字符串类型的 "0" 或 "1",但在 Java 领域模型中,建议将其转换为枚举或布尔值,以防止业务层出现 "0" (String) 被误判为 true 的逻辑错误。
4.1 响应字段映射
| 原始字段 | 业务含义 | 处理建议 |
|---|---|---|
caseRelated | 异常关联 A类 | 高危:建议直接拦截交易 |
fraudTrans | 异常关联 B类 | 高危:建议进入人工复审 |
badCardHolder | 持卡人状态异常 | 中危:建议限制交易额度 |
onlineBlack | 线上渠道异常 | 中危:建议关闭快捷支付 |
4.2 增强型 DTO 示例
Java
public class RiskAssessmentDTO {
private boolean isCaseRelated;
private boolean isFraudTrans;
// 构造器中进行类型转换
public RiskAssessmentDTO(Map<String, Object> rawData) {
this.isCaseRelated = "1".equals(rawData.get("caseRelated"));
this.isFraudTrans = "1".equals(rawData.get("fraudTrans"));
}
// 业务判断逻辑
public boolean shouldBlock() {
return isCaseRelated || isFraudTrans;
}
}
5. 总结
通过 Spring Boot 与原生 Java Crypto 的结合,我们实现了一个轻量级且安全的风险监测客户端。这种AES 加密传输 + 强类型业务封装的设计模式,不仅符合金融级开发的安全规范,也极大地降低了核心业务系统对接外部数据的复杂度。
数据隐私与合规开发提示
在开发涉及用户敏感信息(PII)的功能时,请开发者严格遵守以下合规准则:
- 最小化调用:仅在业务流程(如绑卡、大额提现)必须环节调用接口,避免非必要的批量查询。
- 密钥安全:
Access-Id和Access-Key属于高敏感凭证,严禁硬编码在 Git 仓库中,生产环境应通过 K8s Secrets 或环境变量注入。 - 用户授权:在处理用户银行卡信息前,必须在产品 UI 层面获得用户的明确授权(Opt-in),确保符合《个人信息保护法》相关规定。