登录
原创

人脸实名认证(SM2+SM4国密加密版)- 聚合数据API调用Java示例

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

本文介绍如何使用 Java 调用聚合数据的 人脸比对认证(国密版) 接口,在 SM2+SM4 加密流程基础上,补充 HMAC-SM3 请求签名、人像参数与运行注意点。

该接口使用国密算法(SM2 + SM4 + HMAC-SM3)进行加密与防篡改传输:

  • API 文档(人脸实名认证,含国密版说明): https://www.juhe.cn/docs/api/id/264
  • 请求地址: https://apis.juhe.cn/verifyface/sm
  • 加密方式: SM2 公钥加密 SM4 密钥与 IV + SM4-CBC 加密业务字段 + HMAC-SM3 签名
  • 传输协议: HTTP POST(application/x-www-form-urlencoded

说明: 该接口需在聚合数据完成相应资质与应用审核;SM2 公钥需按文档向商务索取,勿使用自生成密钥

加密与签名流程

1. 随机生成 SM4 密钥(16 字节)+ SM4 IV(16 字节)
2. 使用 SM2 公钥加密 SM4 密钥与 IV → Base64(参数 sm4Key、sm4Iv)
3. 使用 SM4-CBC(PKCS5Padding)加密业务明文 → Base64:
   姓名 realname、身份证号 idcard、人脸图片 Base64 串 image(无 data:image 前缀)
4. 计算 HMAC-SM3(结果 Base64)作为 signature:
   消息 = idcard明文 + realname明文 + image明文 + API Key(key)
   密钥 = 个人中心「账号管理 → 基本信息」中的 OpenId(与 key 不同)
5. 发送 HTTP POST,携带 key、sm4Key、sm4Iv、加密后的 realname/idcard/image、signature;
   可选 thousand=1 表示返回千分制分数

Maven 依赖

pom.xml 中需具备 Bouncy Castle(SM2、SM4、SM3/HMAC-SM3)与 Apache HttpClient 5

<!-- Bouncy Castle:SM2、SM4、SM3(HMAC-SM3) -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

<!-- Apache HttpClient 5 -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.2.1</version>
</dependency>

核心技术要点

1. SM2 加密 — 直接加密 byte[]

SM4 密钥与 IV 为随机二进制,必须用 processBlock(data, 0, data.length) 直接加密,不要先转成 String 再加密,避免非法 UTF-8 序列损坏数据。

聚合侧通常要求密文为 ASN.1/DER 再 Base64,示例中通过 SM2EncryptExample.convertToASN1DER(rawCiphertext) 完成(useDER = true)。

2. SM4-CBC 加密业务字段

算法:SM4/CBC/PKCS5Padding(Bouncy Castle 提供者 BC),明文使用 UTF-8;密文 Base64 后作为表单字段值。

其中 image 的明文为人脸图片文件的 纯 Base64 字符串(不含 data:image/jpg;base64, 前缀)。文档建议 JPEG、体积与分辨率在合理区间(如官方说明的约 16~20K、长边约 800px 等),过大易触发参数错误。

3. HMAC-SM3 签名

// 消息顺序以官方文档为准:idcard + realname + image + key
String message = idcard + realname + imageBase64 + apiKey;
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(openId.getBytes(StandardCharsets.UTF_8)));
// ... update(message UTF-8) → doFinal → Base64
  • 签名密钥: OpenId(个人中心账号基本信息中查看)。
  • 勿混淆: 表单里的 key 是接口 Key;HMAC 的密钥是 OpenId
  • 参与签名的 image: 与送入 SM4 加密的字符串相同,均为未加密的 Base64 人像数据。

4. HTTP 请求参数构建

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("idcard", encryptedIdcard));    // SM4 加密后
params.add(new BasicNameValuePair("realname", encryptedRealname)); // SM4 加密后
params.add(new BasicNameValuePair("image", encryptedImage));       // SM4 加密后
params.add(new BasicNameValuePair("signature", signature));         // HMAC-SM3 Base64
// 可选:params.add(new BasicNameValuePair("thousand", "1"));

成功时返回 JSON 中 result 可能包含 resscoreorderid 及服务端 signature(用于按文档验签),具体字段以官方文档为准。

5. 常见问题

现象 可能原因
公钥读取失败 PEM 路径错误,或使用了非本接口提供的公钥
签名错误 OpenId 填错、消息拼接顺序不是 idcard+realname+image+key、image 与加密前不一致
参数错误 / 图片过大 人像 Base64 过大或不符合格式要求

完整代码实现

集成时请替换 API_KEYOPEN_ID、公钥路径及本地人像路径,切勿将真实密钥提交到版本库

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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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 264)「人脸比对认证(国密版)」:
 * 请求地址 {@code https://apis.juhe.cn/verifyface/sm},POST,
 * {@code Content-Type: application/x-www-form-urlencoded}。
 * </p>
 *
 * @see <a href="https://www.juhe.cn/docs/api/id/264">人脸实名认证 API 文档</a>
 */
public class Example264 {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }
    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文件格式不正确,无法读取公钥");
        }
    }
    private static final String API_URL = "https://apis.juhe.cn/verifyface/sm";
    /** 在个人中心「我的数据」中接口名称上方查看 */
    private static final String API_KEY = "请替换为你的API Key";
//    private static final String API_KEY = "7a312b221dxxxxxxxxxxx288bacb";
    /**
     * 签名密钥:个人中心 → 账号管理 → 基本信息中的 OpenId(用于 HMAC-SM3,与 key 不同)
     */
    private static final String OPEN_ID = "请替换为你的OpenId";
//    private static final String OPEN_ID = "JH285250cxxxxxxxx15221d";


    /**
     * 使用 SM2 公钥加密 byte[](与 {@link Example207} 一致,默认输出 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 {
        // 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");
    }

    /** SM4-CBC + PKCS5Padding,密文 Base64(与 Example207 一致) */
    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 原始值 + image 原始值 + key 值;
     * 签名算法 HMAC-SM3,密钥为 OpenId;结果 Base64。
     */
    public static String hmacSm3SignatureBase64(String openId, String idcard, String realname, String imageBase64, String apiKey)
            throws Exception {
        String message = idcard + realname + imageBase64 + 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 imageBase64 人脸图 Base64,不含 {@code data:image/...;base64,} 前缀
     * @param sm2PublicKeyPemPath 聚合数据提供的 SM2 公钥 PEM 路径(需向商务索取,勿使用自生成密钥)
     * @param thousand   传 {@code "1"} 返回千分制分数,否则百分制;可传 {@code null} 不传该参数
     */
    public static String verifyFace(String realname, String idcard, String imageBase64,
                                    String sm2PublicKeyPemPath, String thousand) 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 encryptedImage = encryptWithSM4CBC(sm4Key, sm4Iv, imageBase64);
        System.out.println("   ✓ 姓名、身份证、人像 Base64 已加密");

        String signature = hmacSm3SignatureBase64(OPEN_ID, idcard, realname, imageBase64, 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("idcard", encryptedIdcard));
        params.add(new BasicNameValuePair("realname", encryptedRealname));
        params.add(new BasicNameValuePair("image", encryptedImage));
        params.add(new BasicNameValuePair("signature", signature));
        if (thousand != null && !thousand.isEmpty()) {
            params.add(new BasicNameValuePair("thousand", thousand));
        }

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

    /**
     * 从 JPG 文件读取字节并转为纯 Base64(无 data URI 前缀)
     */
    public static String imageFileToBase64(Path imagePath) throws IOException {
        byte[] raw = Files.readAllBytes(imagePath);
        return Base64.getEncoder().encodeToString(raw);
    }

    public static void main(String[] args) {
        try {
            //人脸图片路径
            Path faceImage = Paths.get("/path/to/face.jpg");
//            Path faceImage = Paths.get("/Users/java0904/GmSSL-Java/src/main/resources/face.jpg");
            //SM2公钥PEM路径
            String sm2PublicKeyPath = "/path/to/publicKey264.pem";
//            String sm2PublicKeyPath = "/Users/java0904/GmSSL-Java/src/main/resources/publickey264.pem";

            String realname = "奥巴笨";
            String idcard = "431381xxxxxxxxx0021";
            String imageBase64 = imageFileToBase64(faceImage);

            System.out.println("待比对信息:");
            System.out.println("  姓名: " + realname);
            System.out.println("  身份证: " + idcard);
            System.out.println("  人像文件: " + faceImage.toAbsolutePath());
            System.out.println("  人像 Base64 长度: " + imageBase64.length());
            System.out.println();

            // 不传 thousand 则返回百分制;需要千分制可改为 verifyFace(..., "1")
            verifyFace(realname, idcard, imageBase64, sm2PublicKeyPath, null);

            System.out.println("\n=== 请求结束 ===");
        } catch (Exception e) {
            System.err.println("❌ 调用失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}


评论区

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

0

0

3

举报