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;且明确编码意图。中文名字("欧阳娜娜"这种)签名字节必须与服务端解码一致,编码错一位签名必错;Character.forDigit(..., 16) 天然小写;很多老代码用大写 hexDigits 数组,个别服务端做字符串比较时就会 1104;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,四个改进点:
replace 姓名&、+ 等字符时 replace 替换错乱URLEncoderurl + "?" + param 但用 POST 传 bodymall_id / appkey 常量sendPost 无超时控制HttpClient 显式超时三、错误码与排障
返回 JSON 有两层:data.code 是业务结果,status 是服务状态。
1104 签名排查清单(按命中率排序):
tm 用了 10 位秒级,或签名与表单里用了两个不同时刻的值;getBytes() 没显式指定 UTF-8,Windows 默认 GBK 的开发机上必现;四、生产落地四条建议
.gitignore 掉一切本地凭证文件。 leaked appkey 无法撤销重置前,账单是你的;相关资源
合规提醒:实名核验仅限用户已授权的业务场景使用,禁止批量查询他人身份信息。接入方需自行履行《中华人民共和国个人信息保护法》《中华人民共和国数据安全法》项下的数据处理义务。