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 tokens
    private static final String SUMSUB_APP_TOKEN = "YOUR_SUMSUB_APP_TOKEN"; // Example: sbx:uY0CgwELmgUAEYl4hNWxLngb.0wSeQeiYny4WEqmAAALEAik2qTC96fBad
    private static final String SUMSUB_SECRET_KEY = "YOUR_SUMSUB_SECRET_KEY"; // Example: Hej2ch7lkG2kTdiiiUDZFNs05Cilh5Gq

    private 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.com

    private 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 Applicant
        String 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 Initialization
        String 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 Match
        File 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 Status
        System.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 文档.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

故事很腻i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值