接入教程2026-09-16· 10 分钟阅读

Java 接入身份证核验 API 完整指南:签名工具类设计与官方 Demo 的四个改进

Java 后端接入身份证二要素核验接口的完整教程:手写可复用的 MD5 签名工具类(编码/大小写/null 处理三个细节)、JDK 11 HttpClient 零依赖实现、1104 签名排查清单与生产落地建议。

诺正通研究院
本文讲 Java 后端如何接入身份证二要素核验 API:从 MD5 签名工具类写起,到完整可运行的 HTTP 调用,再到官方 Demo 里几个值得商榷的写法。基于诺正通(nztapi.com)接口,其他家的核验 API 流程类似,签名细节以各自文档为准。

前置:这个接口做什么、怎么开通

身份证二要素核验:提交「姓名 + 身份证号」,接口对接权威数据源实时返回两者是否一致。典型用途是注册实名、开户核身、支付绑卡前的身份校验等企业授权场景。

开通流程:在服务商官网提交试用申请(企业信息 + 联系方式)→ 人工资质审核(通常 1 个工作日)→ 发放 mall_id(商户 ID)和 appkey(签名密钥)。appkey 只参与签名计算,绝不随请求传输

核心签名规则(本文所有代码围绕它展开):

sign = md5( mall_id + realname + idcard + tm + appkey )

五个值按文档顺序直接字符串拼接,无分隔符,MD5 小写十六进制。

一、先写一个可复用的 MD5 签名工具类

网上流传的写法大多能跑,但细节暗雷不少。先上推荐版本:

import java.nio.charset.StandardCharsets;

import java.security.MessageDigest;

public final class SignUtil {

/** 32 位小写 MD5。核验接口要求小写十六进制。 */ public static String md5Lower(String text) { try { byte[] bytes = MessageDigest.getInstance("MD5") .digest(text.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(32); for (byte b : bytes) { sb.append(Character.forDigit((b >> 4) & 0xF, 16)); sb.append(Character.forDigit(b & 0xF, 16)); } return sb.toString(); } catch (Exception e) { throw new IllegalStateException("MD5 not available", e); } }

/** 13 位毫秒时间戳,防重放。签名与请求必须用同一个值。 */ public static String tm() { return String.valueOf(System.currentTimeMillis()); } }

三个刻意的设计决策,都是踩坑换来的:

  • StandardCharsets.UTF_8 而不是 "UTF-8" 字符串重载——后者抛受检异常,逼你写 try-catch;且明确编码意图。中文名字("欧阳娜娜"这种)签名字节必须与服务端解码一致,编码错一位签名必错;
  • 小写 hexCharacter.forDigit(..., 16) 天然小写;很多老代码用大写 hexDigits 数组,个别服务端做字符串比较时就会 1104;
  • 不返回 null。官方 Demo 的 md5() 失败时 return null,调用方拿 null 拼签名,最后报的是远端的"签名不合法"——排查方向直接被带偏。本地失败应该第一时间抛异常。
  • 二、完整调用:比官方 Demo 好在哪

    官方文档附的 Java Demo 能跑,但有几个地方值得商榷。先看推荐实现:

    import java.io.IOException;
    

    import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors;

    public class IdcardVerifyClient {

    private static final String VERIFY_URL = "https://api2.nztapi.com/v2/id-server"; private final String mallId; private final String appKey; private final HttpClient http = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build();

    public IdcardVerifyClient(String mallId, String appKey) { this.mallId = mallId; this.appKey = appKey; }

    /** 身份证二要素核验。返回服务端 JSON 字符串。 */ public String verify(String realname, String idcard) throws IOException, InterruptedException { realname = realname.trim(); idcard = idcard.trim().toLowerCase(); // 末位 X 必须小写 String tm = SignUtil.tm(); String sign = SignUtil.md5Lower(mallId + realname + idcard + tm + appKey);

    Map<String, String> form = new LinkedHashMap<>(); form.put("mall_id", mallId); form.put("realname", realname); form.put("idcard", idcard); form.put("tm", tm); form.put("sign", sign);

    String body = form.entrySet().stream() .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) .collect(Collectors.joining("&"));

    HttpRequest req = HttpRequest.newBuilder(URI.create(VERIFY_URL)) .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") .timeout(Duration.ofSeconds(15)) .POST(HttpRequest.BodyPublishers.ofString(body)) .build();

    HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() != 200) { throw new IOException("HTTP " + resp.statusCode() + ": " + resp.body()); } return resp.body(); }

    public static void main(String[] args) throws Exception { String mallId = System.getenv("NZT_MALL_ID"); // 凭证走环境变量 String appKey = System.getenv("NZT_APP_KEY"); // 不要硬编码进源码 if (mallId == null || appKey == null) { System.err.println("请先配置环境变量 NZT_MALL_ID / NZT_APP_KEY"); System.err.println("申请入口: https://www.nztapi.com (1 个工作日人工审核开通)"); return; } System.out.println(new IdcardVerifyClient(mallId, appKey) .verify("张三", "11010119900101123X")); // 成功返回: {"data":{"code":"1000","message":"一致"},"status":"2001"} } }

    纯 JDK 11+,零第三方依赖,Java 8 把 HttpClient 换成 HttpURLConnection 即可。对照官方 Demo,四个改进点:

    官方 Demo 的写法问题本文写法 拼 URL 字符串,手动 replace 姓名中文名含 &+ 等字符时 replace 替换错乱构造表单 Map,统一 URLEncoder url + "?" + param 但用 POST 传 bodyURL 和 body 两套拼接,容易只编码一半只用表单 body,单一事实来源 硬编码 mall_id / appkey 常量源码泄漏 = 密钥泄漏,Git 历史擦不掉环境变量注入 sendPost 无超时控制服务端偶发慢查询会拖死调用方线程池HttpClient 显式超时

    三、错误码与排障

    返回 JSON 有两层:data.code 是业务结果,status 是服务状态。

    data.code含义计费排查动作 1000一致是放行 1001不一致是拒绝或转人工 1002库中无此号否检查输入是否有误 1101商户 ID 不合法否mall_id 错 1103编码不合法否身份证末位 X 小写了吗 1104签名不合法否见下方检查清单 1106余额不足否充值 1107tm 不合法否必须 13 位毫秒 1108参数异常否参数名/取值错;参数无误则确认产品已开通 1109账户被暂停否联系服务商

    1104 签名排查清单(按命中率排序):

  • 拼接顺序和文档不一致——注意:不同接口的签名串不同。比如银行卡三/四要素的签名串不含身份证号字段,手机三要素签名串不含手机号,别凭"直觉"把全部参数拼进去;
  • tm 用了 10 位秒级,或签名与表单里用了两个不同时刻的值;
  • MD5 结果是大写(老工具类常见);
  • 中文姓名编码不一致——getBytes() 没显式指定 UTF-8,Windows 默认 GBK 的开发机上必现;
  • appkey 首尾带了空格(从别处复制凭证的经典事故)。
  • 四、生产落地四条建议

  • 凭证管理:环境变量或配置中心,.gitignore 掉一切本地凭证文件。 leaked appkey 无法撤销重置前,账单是你的;
  • 前置格式校验:18 位身份证正则 + 11 位手机号,格式不对直接拒——错误的调用也可能计费;
  • 超时与重试:客户端设 10-15 秒读超时;只在网络异常时重试一次(业务失败重试 = 双倍计费);
  • 审计留痕:记录每次核验的授权依据、请求参数摘要(不要落原始身份证号明文)、返回码,合规检查时能自证。
  • 相关资源

  • 本文涉及的完整代码已收录进四语言示例仓库(Python/Node/PHP/Go + OpenAPI 3.1 规格 + Postman 集合):github.com/awayaz1987-byte/nztapi-api-docs
  • 参数表、签名机制与错误码总表见通用接入规范
  • 身份证二要素接口的完整契约见产品接入文档
  • 合规提醒:实名核验仅限用户已授权的业务场景使用,禁止批量查询他人身份信息。接入方需自行履行《中华人民共和国个人信息保护法》《中华人民共和国数据安全法》项下的数据处理义务。
    #Java#身份证核验#MD5 签名#HttpClient#实名认证接入

    看完想立刻接入?

    100 次免费调用,1 个工作日开通

    限时新用户 100 次免费

    1 个工作日开通 · 7×24 技术支持