本文介绍如何使用 Java 调用聚合数据的
份证四要素核验(国密版)接口。
- API 文档: https://www.juhe.cn/docs/api/id/550
- 请求地址:
https://apis.juhe.cn/id_card_four_factors/sm - 加密方式: SM2 公钥加密 SM4 密钥与 IV + SM4-CBC 加密四要素明文 + HMAC-SM3 签名
- 传输协议: HTTP POST(
application/x-www-form-urlencoded)
四要素含义: 姓名、身份证号码、有效期开始日期、有效期截止日期。日期明文格式为 yyyyMMdd(如 20170606);长期有效身份证的截止日期填 8 个 0:00000000。
说明: 该接口为企业场景,需完成应用与资质审核;SM2 公钥需按文档向商务索取,勿使用自生成密钥。个人中心 OpenId 用于 HMAC-SM3,与请求参数 key(AppKey)不同。
加密与签名流程
1. 随机生成 SM4 密钥(16 字节)+ SM4 IV(16 字节)
2. 使用 SM2 公钥加密 SM4 密钥与 IV → Base64(sm4Key、sm4Iv)
3. 使用 SM4-CBC(PKCS5Padding)加密以下明文字段 → Base64:
realname、idcard、start_date、end_date
4. 计算 HMAC-SM3(结果 Base64)作为 signature:
消息 = idcard明文 + realname明文 + start_date明文 + end_date明文 + API Key(key)
密钥 = 个人中心「账号管理 → 基本信息」中的 OpenId
5. POST 提交:key、sm4Key、sm4Iv、加密后的 realname/idcard/start_date/end_date、signature
成功时 error_code 为 0;result.res 为 1 表示四要素一致,2 表示不一致。返回中可能包含 result.signature(服务端对 res 与 orderid 等的 HMAC-SM3,可按文档验签)。
Maven 依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
核心技术要点
1. SM2 / SM4
- SM2:
C1C2C3模式,密文转 DER 后 Base64(useDER = true)。 - SM4:
SM4/CBC/PKCS5Padding,明文 UTF-8,密文 Base64。
2. HMAC-SM3 签名
官方规定签名字符串顺序为:
idcard + realname + start_date + end_date + key
(均为参与 SM4 加密前的原始明文字符串;key 为 AppKey。)
String message = idcard + realname + startDate + endDate + apiKey;
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(openId.getBytes(StandardCharsets.UTF_8)));
// UTF-8 编码 message → update → doFinal → Base64
勿混淆: 表单参数 key 是 AppKey;MAC 密钥是 OpenId。
3. HTTP 表单参数名
| 参数 | 说明 |
|---|---|
| key | AppKey |
| sm4Key / sm4Iv | SM2 加密后的 SM4 密钥与 IV(Base64) |
| realname / idcard / start_date / end_date | SM4 加密后的四要素(Base64) |
| signature | HMAC-SM3 结果(Base64) |
4. 错误码(摘录)
| error_code | 说明 |
|---|---|
| 0 | 成功(具体一致与否看 result.res) |
| 255001 | 参数错误 |
| 255003 | 四要素校验失败 |
| 255006 / 255007 | 数据源异常 / 超时 |
系统级 10001–10021 等与聚合数据通用说明一致,见 接口文档页。
5. 常见问题
| 现象 | 可能原因 |
|---|---|
| 签名失败 | OpenId 错误,或拼接顺序不是 idcard+realname+start_date+end_date+key |
| 参数错误 | 日期格式不是 8 位 yyyyMMdd,或长期有效未使用 00000000 |
| 解密/验签失败 | SM2 公钥非本接口专用,或 DER/Base64 与约定不一致 |
完整代码实现
package org.example;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
/**
* 聚合数据 - 身份证四要素核验(国密版,SM2+SM4+HMAC-SM3)示例
* <p>
* 官方 PDF(数据 ID 550)第四节「身份证四要素校验(国密版)」:
* {@code https://apis.juhe.cn/id_card_four_factors/sm},POST,
* {@code Content-Type: application/x-www-form-urlencoded}。
* </p>
* <p>
* 四要素:姓名、身份证号码、有效期开始日期、有效期截止日期(长期有效身份证截止日期填 8 个 0)。
* </p>
*
* @see <a href="https://www.juhe.cn/docs/api/id/550">身份证四要素核验 API 文档</a>
*/
public class Example550 {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String API_URL = "https://apis.juhe.cn/id_card_four_factors/sm";
/** 在个人中心「我的数据」中接口名称上方查看 */
private static final String API_KEY = "bc87c549fxxxxx2xxxxe49f4";
/**
* 签名密钥:个人中心 → 账号管理 → 基本信息中的 OpenId(用于 HMAC-SM3,与 key 不同)
*/
private static final String OPEN_ID = "JH285250cxxxxxxxa515221d";
/**
* 使用 SM2 公钥加密 byte[]
*/
public static String encryptWithSM2(PublicKey publicKey, byte[] data, boolean useDER) throws Exception {
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
ECDomainParameters domainParameters = new ECDomainParameters(
sm2ECParameters.getCurve(),
sm2ECParameters.getG(),
sm2ECParameters.getN()
);
BCECPublicKey bcecPublicKey = (BCECPublicKey) publicKey;
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(
bcecPublicKey.getQ(),
domainParameters
);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C2C3);
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
byte[] rawCiphertext = sm2Engine.processBlock(data, 0, data.length);
byte[] finalCiphertext = useDER
? convertToASN1DER(rawCiphertext)
: rawCiphertext;
return Base64.getEncoder().encodeToString(finalCiphertext);
}
public static byte[] convertToASN1DER(byte[] ciphertext) throws Exception {
// C1: 0x04 + x(32字节) + y(32字节) = 65字节
// C2: 密文数据(变长)
// C3: 哈希值(32字节,SM3)
if (ciphertext.length < 97) { // 65 + 32 = 97最小长度
throw new IllegalArgumentException("密文长度不正确");
}
// 解析C1(椭圆曲线点)
if (ciphertext[0] != 0x04) {
throw new IllegalArgumentException("C1格式错误,应该以0x04开头");
}
byte[] xBytes = Arrays.copyOfRange(ciphertext, 1, 33);
byte[] yBytes = Arrays.copyOfRange(ciphertext, 33, 65);
BigInteger x = new BigInteger(1, xBytes);
BigInteger y = new BigInteger(1, yBytes);
// 解析C2和C3
int c2Length = ciphertext.length - 97; // 总长度 - 65(C1) - 32(C3)
byte[] c2 = Arrays.copyOfRange(ciphertext, 65, 65 + c2Length);
byte[] c3 = Arrays.copyOfRange(ciphertext, 65 + c2Length, ciphertext.length);
// 构造ASN.1结构
ASN1EncodableVector vector = new ASN1EncodableVector();
vector.add(new ASN1Integer(x));
vector.add(new ASN1Integer(y));
vector.add(new DEROctetString(c3)); // hash
vector.add(new DEROctetString(c2)); // ciphertext
DERSequence derSequence = new DERSequence(vector);
return derSequence.getEncoded("DER");
}
public static PublicKey loadPublicKeyFromPEM(String pemFilePath) throws IOException {
try (FileReader fileReader = new FileReader(pemFilePath);
PEMParser pemParser = new PEMParser(fileReader)) {
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
if (object instanceof org.bouncycastle.asn1.x509.SubjectPublicKeyInfo) {
return converter.getPublicKey((org.bouncycastle.asn1.x509.SubjectPublicKeyInfo) object);
}
throw new IllegalArgumentException("PEM文件格式不正确,无法读取公钥");
}
}
public static String encryptWithSM4CBC(byte[] key, byte[] iv, String plaintext) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ciphertext);
}
public static byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
return bytes;
}
/**
* 文档:签名的消息内容为 idcard 原始值 + realname 原始值 + start_date 原始值 + end_date 原始值 + key 值;
* HMAC-SM3,密钥为 OpenId;结果 Base64。
*/
public static String hmacSm3SignatureBase64(String openId, String idcard, String realname,
String startDate, String endDate, String apiKey) throws Exception {
String message = idcard + realname + startDate + endDate + apiKey;
HMac hmac = new HMac(new SM3Digest());
byte[] keyBytes = openId.getBytes(StandardCharsets.UTF_8);
hmac.init(new KeyParameter(keyBytes));
byte[] msg = message.getBytes(StandardCharsets.UTF_8);
hmac.update(msg, 0, msg.length);
byte[] out = new byte[hmac.getMacSize()];
hmac.doFinal(out, 0);
return Base64.getEncoder().encodeToString(out);
}
/**
* @param realname 姓名(明文)
* @param idcard 身份证号(明文)
* @param startDate 有效期开始,原文格式 {@code yyyyMMdd},如 {@code 20170606}
* @param endDate 有效期截止,格式同上;长期有效填 {@code 00000000}
* @param sm2PublicKeyPemPath 聚合数据提供的 SM2 公钥 PEM(需向商务索取)
*/
public static String verifyIdCardFourFactors(String realname, String idcard,
String startDate, String endDate,
String sm2PublicKeyPemPath) throws Exception {
System.out.println("=== 身份证四要素核验(国密版)===\n");
byte[] sm4Key = generateRandomBytes(16);
byte[] sm4Iv = generateRandomBytes(16);
System.out.println("1. 生成 SM4 密钥与 IV:");
System.out.println(" SM4密钥(Hex): " + bytesToHex(sm4Key));
System.out.println(" SM4 IV(Hex): " + bytesToHex(sm4Iv));
System.out.println("\n2. 读取 SM2 公钥: " + sm2PublicKeyPemPath);
PublicKey sm2PublicKey = loadPublicKeyFromPEM(sm2PublicKeyPemPath);
System.out.println(" ✓ 公钥加载成功");
boolean useDER = true;
System.out.println("\n3. SM2 加密 SM4 密钥与 IV (格式: " + (useDER ? "ASN.1/DER" : "C1C2C3") + "):");
String encryptedSm4Key = encryptWithSM2(sm2PublicKey, sm4Key, useDER);
String encryptedSm4Iv = encryptWithSM2(sm2PublicKey, sm4Iv, useDER);
System.out.println(" ✓ 完成");
System.out.println("\n4. SM4-CBC 加密业务字段:");
String encryptedRealname = encryptWithSM4CBC(sm4Key, sm4Iv, realname);
String encryptedIdcard = encryptWithSM4CBC(sm4Key, sm4Iv, idcard);
String encryptedStart = encryptWithSM4CBC(sm4Key, sm4Iv, startDate);
String encryptedEnd = encryptWithSM4CBC(sm4Key, sm4Iv, endDate);
System.out.println(" ✓ 姓名、身份证、起止日期已加密");
String signature = hmacSm3SignatureBase64(OPEN_ID, idcard, realname, startDate, endDate, API_KEY);
System.out.println("\n5. HMAC-SM3 签名已计算");
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("key", API_KEY));
params.add(new BasicNameValuePair("sm4Key", encryptedSm4Key));
params.add(new BasicNameValuePair("sm4Iv", encryptedSm4Iv));
params.add(new BasicNameValuePair("realname", encryptedRealname));
params.add(new BasicNameValuePair("idcard", encryptedIdcard));
params.add(new BasicNameValuePair("start_date", encryptedStart));
params.add(new BasicNameValuePair("end_date", encryptedEnd));
params.add(new BasicNameValuePair("signature", signature));
System.out.println("\n6. POST " + API_URL);
String response = sendPostRequest(API_URL, params);
System.out.println("\n7. 响应:");
System.out.println(response);
return response;
}
private static String sendPostRequest(String url, List<NameValuePair> params) throws IOException, ParseException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
return httpClient.execute(httpPost, response -> {
int statusCode = response.getCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println(" HTTP 状态码: " + statusCode);
return responseBody;
});
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static void main(String[] args) {
try {
// ⚠️注意,这里要替换为聚合数据提供的公钥!!!
String sm2PublicKeyPath = "/Users/java0904/juhe-api-example/src/main/resources/publickey550.pem";
String realname = "肖xxx";
String idcard = "431xxxxxxxxxxxxx";
String startDate = "20170606";
String endDate = "20270606";
System.out.println("待核验四要素:");
System.out.println(" 姓名: " + realname);
System.out.println(" 身份证: " + idcard);
System.out.println(" 有效期起: " + startDate);
System.out.println(" 有效期止: " + endDate + "(长期有效请改为 00000000)");
System.out.println();
verifyIdCardFourFactors(realname, idcard, startDate, endDate, sm2PublicKeyPath);
System.out.println("\n=== 请求结束 ===");
} catch (Exception e) {
System.err.println("❌ 调用失败: " + e.getMessage());
e.printStackTrace();
}
}
}