当前位置: 首页 > news >正文

Sumsub 活体检测与人证对比 Java Demo

1. 简介

本项目提供了一个 Java Demo,用于演示如何与 Sumsub API 进行交互,以实现活体检测和人证对比功能。Sumsub 是一套身份验证和 KYC/AML 解决方案,可帮助企业验证用户身份并防范欺诈。

此 Demo 主要展示后端 API 的集成流程,包括:

  • 创建申请人 (Applicant):在 Sumsub 系统中为您的用户创建一个唯一的申请人记录。
  • 获取访问令牌 (Access Token):生成一个用于初始化 Sumsub WebSDK 或 MobileSDK 的访问令牌。客户端 SDK 负责采集用户的活体数据(如自拍照、视频)和身份证件图像。
  • 上传身份证件 (ID Document):通过 API 上传用户的身份证件图像,用于后续的人脸比对和证件真实性校验。
  • 获取申请状态 (Applicant Status):查询申请人的验证状态,了解活体检测和人证对比的结果。

重要提示:实际的活体检测(Liveness Check)和证件图像采集通常由前端的 Sumsub SDK(WebSDK 或 MobileSDK)完成。本 Demo 侧重于后端 API 如何配合 SDK 完成整个验证流程。

2. Demo代码

以下是 SumsubLivenessFaceMatchDemo.java 的核心代码。您可以从附件中下载完整的 Java 文件。

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.apache.commons.codec.binary.Hex;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;public class SumsubLivenessFaceMatchDemo {// TODO: Replace with your Sumsub App Token and Secret Key// You can find them in your Sumsub dashboard: https://cockpit.sumsub.com/ -> Developers -> API tokensprivate static final String SUMSUB_APP_TOKEN = "YOUR_SUMSUB_APP_TOKEN"; // Example: sbx:uY0CgwELmgUAEYl4hNWxLngb.0wSeQeiYny4WEqmAAALEAik2qTC96fBadprivate static final String SUMSUB_SECRET_KEY = "YOUR_SUMSUB_SECRET_KEY"; // Example: Hej2ch7lkG2kTdiiiUDZFNs05Cilh5Gqprivate static final String SUMSUB_BASE_URL = "https://api.sumsub.com"; // For production, use https://api.sumsub.com. For sandbox, use https://api.cluster-test.sumsub.comprivate static final OkHttpClient client = new OkHttpClient();private static final ObjectMapper objectMapper = new ObjectMapper();public static void main(String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InterruptedException {System.out.println("Sumsub Liveness and Face Match Demo");if ("YOUR_SUMSUB_APP_TOKEN".equals(SUMSUB_APP_TOKEN) || "YOUR_SUMSUB_SECRET_KEY".equals(SUMSUB_SECRET_KEY)) {System.err.println("Please replace YOUR_SUMSUB_APP_TOKEN and YOUR_SUMSUB_SECRET_KEY with your actual credentials.");return;}// Step 1: Create an ApplicantString externalUserId = "java-demo-" + UUID.randomUUID().toString();String levelName = "basic-kyc-level"; String applicantId = createApplicant(externalUserId, levelName);System.out.println("Applicant created with ID: " + applicantId + " and externalUserId: " + externalUserId);// Step 2: Generate an Access Token for SDK InitializationString accessToken = getAccessToken(externalUserId, levelName);System.out.println("Access Token for SDK: " + accessToken);System.out.println("Use this token to initialize Sumsub SDK (Web/Mobile) to perform liveness check.");// Step 3: Add an ID Document for Face MatchFile idDocumentFile = new File("id_document_front.jpg"); if (!idDocumentFile.exists()) {idDocumentFile.createNewFile(); System.out.println("Created a dummy file: " + idDocumentFile.getAbsolutePath() + ". Replace with a real ID document image.");}String imageId = addDocument(applicantId, idDocumentFile, "ID_CARD_FRONT");System.out.println("ID Document uploaded with imageId: " + imageId);System.out.println("After the user completes the liveness check via SDK and document is submitted, Sumsub will perform verification.");// Step 4: Get Applicant Verification StatusSystem.out.println("Waiting for a few seconds before checking status (in a real scenario, use webhooks or poll appropriately)...");Thread.sleep(10000); String applicantStatus = getApplicantStatus(applicantId);System.out.println("Applicant Status: " + applicantStatus);System.out.println("Check the Sumsub dashboard for detailed verification results including liveness and face match.");System.out.println("Demo finished. Remember to configure your verification levels in the Sumsub dashboard.");}private static String createApplicant(String externalUserId, String levelName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {String path = "/resources/applicants";String method = "POST";long ts = Instant.now().getEpochSecond();Map<String, String> queryParams = new HashMap<>();queryParams.put("levelName", levelName);Map<String, Object> requestBodyMap = new HashMap<>();requestBodyMap.put("externalUserId", externalUserId);String requestBodyJson = objectMapper.writeValueAsString(requestBodyMap);Request request = buildRequest(method, path, ts, queryParams, RequestBody.create(requestBodyJson, MediaType.parse("application/json")));try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response + " " + response.body().string());Map<String, Object> responseMap = objectMapper.readValue(response.body().string(), Map.class);return (String) responseMap.get("id");}}private static String getAccessToken(String externalUserId, String levelName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {String path = "/resources/accessTokens";String method = "POST";long ts = Instant.now().getEpochSecond();Map<String, String> queryParams = new HashMap<>();queryParams.put("userId", externalUserId);queryParams.put("levelName", levelName);Request request = buildRequest(method, path, ts, queryParams, null);try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response + " " + response.body().string());Map<String, Object> responseMap = objectMapper.readValue(response.body().string(), Map.class);return (String) responseMap.get("token");}}private static String addDocument(String applicantId, File file, String idDocType) throws IOException, NoSuchAlgorithmException, InvalidKeyException {String path = "/resources/applicants/" + applicantId + "/info/idDoc";String method = "POST";long ts = Instant.now().getEpochSecond();RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("metadata", "{"idDocType": "" + idDocType + "", "country": "GBR"}").addFormDataPart("content", file.getName(), RequestBody.create(file, MediaType.parse("image/jpeg"))).build();Request request = buildRequest(method, path, ts, null, requestBody);try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response + " " + response.body().string());Map<String, Object> responseMap = objectMapper.readValue(response.body().string(), Map.class);return (String) responseMap.get("imageId");}}private static String getApplicantStatus(String applicantId) throws IOException, NoSuchAlgorithmException, InvalidKeyException {String path = "/resources/applicants/" + applicantId + "/requiredIdDocsStatus";String method = "GET";long ts = Instant.now().getEpochSecond();Request request = buildRequest(method, path, ts, null, null);try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response + " " + response.body().string());return response.body().string();}}private static Request buildRequest(String method, String path, long ts, Map<String, String> queryParams, RequestBody body) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {HttpUrl.Builder urlBuilder = HttpUrl.parse(SUMSUB_BASE_URL + path).newBuilder();if (queryParams != null) {for (Map.Entry<String, String> entry : queryParams.entrySet()) {urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());}}HttpUrl url = urlBuilder.build();Request.Builder requestBuilder = new Request.Builder().url(url);if (body != null && ("POST".equals(method) || "PATCH".equals(method) || "PUT".equals(method))) {requestBuilder.method(method, body);} else if ("GET".equals(method)) {requestBuilder.get();} else {requestBuilder.method(method, null);}String signature = createSignature(ts, method, path, queryParams, body != null ? bodyToString(body) : null);requestBuilder.addHeader("X-App-Token", SUMSUB_APP_TOKEN);requestBuilder.addHeader("X-App-Access-Sig", signature);requestBuilder.addHeader("X-App-Access-Ts", String.valueOf(ts));requestBuilder.addHeader("Accept", "application/json");return requestBuilder.build();}private static String createSignature(long ts, String httpMethod, String httpPath, Map<String, String> httpQuery, String httpBody) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {StringBuilder queryStringBuilder = new StringBuilder();if (httpQuery != null && !httpQuery.isEmpty()) {for (Map.Entry<String, String> entry : httpQuery.entrySet()) {if (queryStringBuilder.length() > 0) {queryStringBuilder.append("&");}queryStringBuilder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));queryStringBuilder.append("=");queryStringBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));}}String fullPath = httpPath;if (queryStringBuilder.length() > 0) {fullPath += "?" + queryStringBuilder.toString();}String dataToSign = ts + httpMethod.toUpperCase() + fullPath + (httpBody != null ? httpBody : "");Mac mac = Mac.getInstance("HmacSHA256");SecretKeySpec secretKeySpec = new SecretKeySpec(SUMSUB_SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256");mac.init(secretKeySpec);byte[] hmacSha256 = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));return Hex.encodeHexString(hmacSha256);}private static String bodyToString(final RequestBody request) {try {final okio.Buffer buffer = new okio.Buffer();if (request != null) request.writeTo(buffer);else return "";return buffer.readUtf8();} catch (final IOException e) {return "did not work";}}
}

3. 使用说明

3.1 配置凭证

在运行 Demo 之前,您需要将代码中的占位符替换为您的真实 Sumsub 凭证:

  • YOUR_SUMSUB_APP_TOKEN:您的 Sumsub App Token。
  • YOUR_SUMSUB_SECRET_KEY:您的 Sumsub Secret Key。

您可以在 Sumsub 后台的 “Developers” → “API tokens” 部分找到这些凭证。

3.2 配置验证级别 (Verification Level)

确保您在 Sumsub 后台配置了一个验证级别 (例如,代码中使用的 basic-kyc-level),并且该级别包含了以下检查步骤:

  • Selfie (自拍照/活体检测)
  • ID Document (身份证件,例如身份证、护照等)

3.3 准备身份证件图像

Demo 中包含上传身份证件的步骤。请准备一张身份证件的正面照片(例如 id_document_front.jpg),并将其放置在 Demo 项目的根目录下,或者修改代码中的文件路径。

3.4 添加依赖库

运行此 Java Demo 需要以下依赖库:

  • OkHttp: 用于发送 HTTP 请求与 Sumsub API 进行通信。
  • Jackson Databind: 用于处理 JSON 数据的序列化和反序列化。
  • Apache Commons Codec: 用于在生成 API 请求签名时进行 Hex 编码。

您可以通过 Maven 或 Gradle 将这些依赖添加到您的项目中。

Maven (pom.xml):

<dependencies><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.9.3</version> <!-- 建议使用较新稳定版本 --></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.13.3</version> <!-- 建议使用较新稳定版本 --></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.15</version> <!-- 建议使用较新稳定版本 --></dependency>
</dependencies>

Gradle (build.gradle):

dependencies {implementation 'com.squareup.okhttp3:okhttp:4.9.3' // 建议使用较新稳定版本implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' // 建议使用较新稳定版本implementation 'commons-codec:commons-codec:1.15' // 建议使用较新稳定版本
}

3.5 运行 Demo

配置完成后,您可以编译并运行 SumsubLivenessFaceMatchDemo.java。Demo 将依次执行创建申请人、获取访问令牌、上传证件和查询状态等操作,并在控制台输出相关信息。

4. 注意事项

  • 错误处理:本 Demo 为了简洁,对错误处理部分进行了简化。在生产环境中,请务必添加更完善的错误捕获和处理机制。
  • Webhooks:为了及时获取验证状态的更新,推荐在生产环境中使用 Sumsub 的 Webhook 功能,而不是依赖轮询。
  • 安全性:妥善保管您的 SUMSUB_SECRET_KEY,不要将其硬编码到客户端应用程序或公开的代码仓库中。
  • API 文档:有关 Sumsub API 的更多详细信息,请参阅 Sumsub 官方 API 文档.

相关文章:

  • zabbix7.2 zabbix-agent自动注册 被动模式(五)
  • 层序遍历(BFS)核心逻辑:从二叉树到复杂题型的一通百通
  • 初识Linux · IP分片
  • 牛客网 NC22167: 多组数据a+b
  • ROS--NAVI DWA
  • 牛客网刷题:NC208813求逆序数
  • 深度学习之用CelebA_Spoof数据集搭建一个活体检测-一些模型训练中的改动带来的改善
  • Linux系统——进程结束时退出的分析与总结(关于wait与waitpid函数)
  • 扣子(Coze)案例:工作流生成小红书心理学卡片
  • 测序的原理
  • 鸿蒙OSUniApp 实现的地图定位与导航功能#三方框架 #Uniapp
  • 5月15日day26打卡
  • Spring Boot 拦截器:解锁5大实用场景
  • 移动端网络调试全流程:从常见抓包工具到Sniffmaster 的实战体验
  • 小刚说C语言刷题—1088求两个数M和N的最大公约数
  • 每周靶点:TIGIT、ICAM1及文献分享
  • 嵌入式自学第二十二天(5.15)
  • 21、工业大数据分析与实时告警 (模拟根因分析) - /数据与物联网组件/bigdata-root-cause-analysis
  • 线程的两种实现方式
  • 鸿蒙OSUniApp实现的倒计时功能与倒计时组件(鸿蒙系统适配版)#三方框架 #Uniapp
  • 马上评|家长抱婴儿值护学岗,如何避免“被自愿”?
  • 金融月评|尽早增强政策力度、调整施策点
  • 大环线呼之欲出,“金三角”跑起来了
  • 龚正市长调研闵行区,更加奋发有为地稳增长促转型,久久为功增强发展后劲
  • 机构发布“2025中国高职院校排名”
  • 鄂州交警通报致1死2伤车祸:女子操作不当引发,已被刑拘