登录
原创

身份证四要素校验(国密版)(SM2+SM4国密加密版)- 聚合数据API调用Java示例

发布于 2026-04-20 阅读 903
  • 后端
原创

使用 Java 调用聚合数据身份证四要素校验(国密版)id_card_four_factors/sm,SM2 封装 sm4secret、SM4-CBC 加密四要素、HMAC-SM3 请求签名。业务字段为姓名、身份证号、有效期起止(yyyyMMdd)。

  • 请求地址: https://apis.juhe.cn/id_card_four_factors/sm
  • 加密: SM2 公钥加密「16 字节 SM4 密钥 + 16 字节 IV」拼接体 → sm4secret(Base64);四要素为 SM4-CBC 密文(Base64);HMAC-SM3 签名
  • 传输: HTTP POST,Content-Type: application/x-www-form-urlencoded

四要素: 姓名、身份证号、有效期开始有效期结束。日期为 yyyyMMdd;长期有效时结束日期为 00000000

说明: 企业类接口,需资质;SM2 公钥向商务获取。OpenId 用于 HMAC,与参数 key(AppKey)不同。

流程摘要

1. 随机 16 字节 SM4 密钥 + 16 字节 IV,拼接为 byte[],SM2 公钥加密后 Base64 → sm4secret(密文 C1C2C3 经 ASN.1/DER 再 Base64)
2. 四要素明文以 SM4-CBC(PKCS5),UTF-8,分别加密后 Base64
3. HMAC-SM3:消息 = idcard+realname+start_date+end_date+key,密钥 = OpenId,结果 Base64 → signature
4. POST:key、sm4secret、四个加密字段、signature

成功时 error_code=0result.res 为 1 一致、2 不一致。result.signature 为服务端对返回数据的签名,与请求中 signature 不同。

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>

完整代码

API_KEYOPEN_ID 与 PEM 路径改为真实值;下述为脱敏示例。

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;

/**
 * 身份证四要素(国密版):/id_card_four_factors/sm,JSON。
 * 参数 key、sm4secret、四要素密文、signature 等。返回 error_code、result 等;result.res 1/2 表示是否一致。
 */
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 用,与 key 不同)
     */
    private static final String OPEN_ID = "JH285250cxxxxxxxa515221d";

    /** SM2 公钥加密,默认 ASN.1/DER + Base64 */
    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 {
        if (ciphertext.length < 97) {
            throw new IllegalArgumentException("密文长度不正确");
        }
        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);

        int c2Length = ciphertext.length - 97;
        byte[] c2 = Arrays.copyOfRange(ciphertext, 65, 65 + c2Length);
        byte[] c3 = Arrays.copyOfRange(ciphertext, 65 + c2Length, ciphertext.length);

        ASN1EncodableVector vector = new ASN1EncodableVector();
        vector.add(new ASN1Integer(x));
        vector.add(new ASN1Integer(y));
        vector.add(new DEROctetString(c3));
        vector.add(new DEROctetString(c2));

        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;
    }

    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);
    }

    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. SM4 密钥与 IV 拼接后 SM2 加密为 sm4secret (格式: " + (useDER ? "ASN.1/DER" : "C1C2C3") + "):");
        byte[] keyAndIv = new byte[sm4Key.length + sm4Iv.length];
        System.arraycopy(sm4Key, 0, keyAndIv, 0, sm4Key.length);
        System.arraycopy(sm4Iv, 0, keyAndIv, sm4Key.length, sm4Iv.length);
        String sm4secret = encryptWithSM2(sm2PublicKey, keyAndIv, 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("sm4secret", sm4secret));
        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 = "/path/to/publickey550.pem";
            String realname = "赵xx";
            String idcard = "2201xxxxxx016";
            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();
        }
    }
}

评论区

眉上的汗水,眉下的泪水,你总要选择一样

0

0

3

举报