• Java集成腾讯云OCR身份证识别接口


    一、背景

            项目用到身份证识别获取人员信息的功能,于是想到了腾讯云提供这样的API。在整合代码过程都很顺利,利用腾讯云官方SDK很快集成进来。但是在上测试环境部署时有了新的问题,通过Nginx代理后的环境无法访问到目标腾讯云接口,遂有了如下的改造过程。

    二、SDK集成Demo

            首先是Maven依赖树:

    1. <dependency>
    2. <groupId>com.tencentcloudapigroupId>
    3. <artifactId>tencentcloud-sdk-javaartifactId>
    4. <version>4.0.11version>
    5. dependency>

            然后是腾讯云提供的调试代码,改造了一部分:

    1. import com.tencentcloudapi.common.Credential;
    2. import com.tencentcloudapi.common.exception.TencentCloudSDKException;
    3. import com.tencentcloudapi.common.profile.ClientProfile;
    4. import com.tencentcloudapi.common.profile.HttpProfile;
    5. import com.tencentcloudapi.ocr.v20181119.OcrClient;
    6. import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRRequest;
    7. import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRResponse;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.beans.factory.annotation.Value;
    10. import org.springframework.context.annotation.PropertySource;
    11. import org.springframework.stereotype.Component;
    12. import java.text.ParseException;
    13. @Component
    14. @Slf4j
    15. @PropertySource("classpath:/properties/tencentCard.properties")
    16. public class TencentUtil {
    17. @Value("${secretId}")
    18. private String secretId;
    19. @Value("${secretKey}")
    20. private String secretKey;
    21. @Value("${tencentUrl}")
    22. private String tencentUrl;
    23. public RecognitionView recognition(String cBase64) {
    24. if (cBase64.length() > 10485760) {
    25. throw new BusinessException("证件识别失败:图片文件太大");
    26. }
    27. RecognitionView tRecognitionView = new RecognitionView();
    28. try{
    29. // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密
    30. // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305
    31. // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
    32. Credential cred = new Credential(secretId, secretKey);
    33. // 实例化一个http选项,可选的,没有特殊需求可以跳过
    34. HttpProfile httpProfile = new HttpProfile();
    35. httpProfile.setEndpoint(tencentUrl);
    36. // 实例化一个client选项,可选的,没有特殊需求可以跳过
    37. ClientProfile clientProfile = new ClientProfile();
    38. clientProfile.setHttpProfile(httpProfile);
    39. // 实例化要请求产品的client对象,clientProfile是可选的
    40. OcrClient client = new OcrClient(cred, "ap-beijing", clientProfile);
    41. // 实例化一个请求对象,每个接口都会对应一个request对象
    42. IDCardOCRRequest req = new IDCardOCRRequest();
    43. req.setImageBase64(cBase64);
    44. // 返回的resp是一个IDCardOCRResponse的实例,与请求对象对应
    45. IDCardOCRResponse resp = client.IDCardOCR(req);
    46. tRecognitionView.setRecognitionView(resp);
    47. // 输出json格式的字符串回包
    48. log.info("证件识别返回参数:" + IDCardOCRResponse.toJsonString(resp));
    49. } catch (Exception e) {
    50. String tError = "证件识别失败:" + e.getMessage();
    51. log.error(tError);
    52. throw new BusinessException(tError);
    53. }
    54. return tRecognitionView;
    55. }
    56. }

            postman调试后可以正常获取到解析内容

    三、Nginx调用失败及解决

            部署到测试环境后,由于内网服务器需要代理到外网服务器进行外网地址的访问,此时便提示证书找不到的错误。

            找问题的过程很坎坷,从证书的有效性、代理的连通性、SDK的限制性等等,研究了将近三天,就连做梦都在思考哪里有问题。最后实在没了方向,决定从根上入手,跳过证书验证。

            跳过验证分为两步,1、放弃SDK请求方式,需要手写Connection;2、增加跳过证书的代码逻辑。于是便有了如下代码:

    1. import com.alibaba.fastjson.JSON;
    2. import com.google.gson.Gson;
    3. import com.google.gson.reflect.TypeToken;
    4. import com.tencentcloudapi.common.Credential;
    5. import com.tencentcloudapi.common.JsonResponseModel;
    6. import com.tencentcloudapi.common.profile.ClientProfile;
    7. import com.tencentcloudapi.common.profile.HttpProfile;
    8. import com.tencentcloudapi.ocr.v20181119.OcrClient;
    9. import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRRequest;
    10. import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRResponse;
    11. import lombok.extern.slf4j.Slf4j;
    12. import okhttp3.*;
    13. import org.apache.commons.codec.binary.Base64;
    14. import org.springframework.beans.factory.annotation.Value;
    15. import org.springframework.context.annotation.PropertySource;
    16. import org.springframework.stereotype.Component;
    17. import javax.crypto.Mac;
    18. import javax.crypto.spec.SecretKeySpec;
    19. import javax.xml.bind.DatatypeConverter;
    20. import java.lang.reflect.Type;
    21. import java.nio.charset.Charset;
    22. import java.nio.charset.StandardCharsets;
    23. import java.security.MessageDigest;
    24. import java.text.SimpleDateFormat;
    25. import java.util.*;
    26. @Component
    27. @Slf4j
    28. @PropertySource("classpath:/properties/tencentCard.properties")
    29. public class TencentUtil {
    30. @Value("${secretId}")
    31. private String secretId;
    32. @Value("${secretKey}")
    33. private String secretKey;
    34. @Value("${tencentUrl}")
    35. private String tencentUrl;
    36. private final static String CT_JSON = "application/json; charset=utf-8"; // 请求头内容类型
    37. private final static Charset UTF8 = StandardCharsets.UTF_8; // 编码格式
    38. static final OkHttpClient HTTP_CLIENT = new SslUtils().getUnsafeOkHttpClient();
    39. public RecognitionView recognitionPassSSL(byte[] cbyte) {
    40. String ImageBase64 = Base64.encodeBase64String(cbyte);
    41. Map tRequest = new HashMap<>();
    42. tRequest.put("ImageBase64", ImageBase64);
    43. String requestData = JSON.toJSONString(tRequest);
    44. RecognitionView tRecognitionView = new RecognitionView();
    45. try {
    46. MediaType mediaType = MediaType.parse(CT_JSON);
    47. RequestBody body = RequestBody.create(mediaType, requestData);
    48. Request.Builder tBuilder = new Request.Builder()
    49. .url("https://" + tencentUrl)
    50. .method("POST", body);
    51. this.assembleHeader(tBuilder,this.sign(requestData));
    52. Request request = tBuilder.build();
    53. Response response = HTTP_CLIENT.newCall(request).execute();
    54. String tResult = response.body().string();
    55. Gson gson = new Gson();
    56. log.info("证件识别返回参数:" + tResult);
    57. if (tResult.contains("Error")) {
    58. //{"Response":{"RequestId":"4dd26ba4-3e0e-412b-b5a3-047829d5541f","Error":{"Code":"FailedOperation.ImageNoIdCard","Message":"照片未检测到身份证"}}}
    59. JsonResponseModel resp = JSON.parseObject(tResult,JsonResponseModel.class);
    60. TencentErrorView tTencentErrorView = JSON.parseObject(JSON.toJSONString(resp.response),TencentErrorView.class);
    61. throw new BusinessException(tTencentErrorView.getError().getMessage());
    62. } else {
    63. //{"name": "吕能仕","id": "362323194911046513","nation": "汉","sex": "男","birthDay": "1949-11-04","address": "江西省上饶市玉山县四股桥乡丁村村喻村4号","age_unit": "岁","age_value": "73"}
    64. Type type = new TypeToken>() {}.getType();
    65. JsonResponseModel resp = gson.fromJson(tResult, type);
    66. tRecognitionView.setRecognitionView(resp.response);
    67. }
    68. } catch (Exception e) {
    69. String tError = "证件识别失败:" + e.getMessage();
    70. log.error(tError);
    71. throw new BusinessException(tError);
    72. }
    73. return tRecognitionView;
    74. }
    75. private void assembleHeader(Request.Builder tBuilder, Map sign) {
    76. Set strings = sign.keySet();
    77. for (String tName : strings) {
    78. tBuilder.addHeader(tName, sign.get(tName));
    79. }
    80. }
    81. public RecognitionView recognition(byte[] cbyte) {
    82. String tBase64 = Base64.encodeBase64String(cbyte);
    83. RecognitionView tRecognitionView = new RecognitionView();
    84. try {
    85. // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密
    86. // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305
    87. // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
    88. Credential cred = new Credential(secretId, secretKey);
    89. // 实例化一个http选项,可选的,没有特殊需求可以跳过
    90. HttpProfile httpProfile = new HttpProfile();
    91. httpProfile.setEndpoint(tencentUrl);
    92. // 实例化一个client选项,可选的,没有特殊需求可以跳过
    93. ClientProfile clientProfile = new ClientProfile();
    94. clientProfile.setHttpProfile(httpProfile);
    95. // 实例化要请求产品的client对象,clientProfile是可选的
    96. OcrClient client = new OcrClient(cred, "ap-beijing", clientProfile);
    97. // 实例化一个请求对象,每个接口都会对应一个request对象
    98. IDCardOCRRequest req = new IDCardOCRRequest();
    99. req.setImageBase64(tBase64);
    100. // 返回的resp是一个IDCardOCRResponse的实例,与请求对象对应
    101. IDCardOCRResponse resp = client.IDCardOCR(req);
    102. tRecognitionView.setRecognitionView(resp);
    103. // 输出json格式的字符串回包
    104. log.info("证件识别返回参数:" + IDCardOCRResponse.toJsonString(resp));
    105. } catch (Exception e) {
    106. String tError = "证件识别失败:" + e.getMessage();
    107. log.error(tError);
    108. throw new BusinessException(tError);
    109. }
    110. return tRecognitionView;
    111. }
    112. /**
    113. * API签名方法
    114. * @param data 发送的json串数据
    115. * @return 请求头map
    116. * @throws Exception 异常
    117. */
    118. @SuppressWarnings({"JsonStandardCompliance", "DuplicatedCode"})
    119. private Map sign(String data) throws Exception {
    120. String service = "ocr"; // 腾讯云服务器
    121. String host = "ocr.tencentcloudapi.com"; // 服务器地址
    122. String region = "ap-beijing"; // 服务器区域
    123. String action = "IDCardOCR"; // api接口名称
    124. String version = "2018-11-19"; // 接口版本号
    125. String algorithm = "TC3-HMAC-SHA256";
    126. // String timestamp = "1551113065";
    127. String timestamp = String.valueOf(System.currentTimeMillis() / 1000); // 时间戳
    128. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    129. // 注意时区,否则容易出错
    130. sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    131. String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
    132. // ************* 步骤 1:拼接规范请求串 *************
    133. String httpRequestMethod = "POST"; // 请求方法
    134. String canonicalUri = "/";
    135. String canonicalQueryString = "";
    136. String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n"; // 请求头信息
    137. String signedHeaders = "content-type;host"; // 签名头包含内容
    138. String payload = data; // 请求内容
    139. String hashedRequestPayload = sha256Hex(payload);
    140. String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
    141. + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
    142. // ************* 步骤 2:拼接待签名字符串 *************
    143. String credentialScope = date + "/" + service + "/" + "tc3_request";
    144. String hashedCanonicalRequest = sha256Hex(canonicalRequest);
    145. String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
    146. // ************* 步骤 3:计算签名 *************
    147. byte[] secretDate = hmac256(("TC3" + secretKey).getBytes(UTF8), date);
    148. byte[] secretService = hmac256(secretDate, service);
    149. byte[] secretSigning = hmac256(secretService, "tc3_request");
    150. String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
    151. // ************* 步骤 4:拼接 Authorization *************
    152. String authorization = algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", "
    153. + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
    154. TreeMap headers = new TreeMap();
    155. headers.put("Authorization", authorization);
    156. headers.put("Content-Type", CT_JSON);
    157. headers.put("Host", host);
    158. headers.put("X-TC-Action", action);
    159. headers.put("X-TC-Timestamp", timestamp);
    160. headers.put("X-TC-Version", version);
    161. headers.put("X-TC-Region", region);
    162. return headers;
    163. }
    164. private static byte[] hmac256(byte[] key, String msg) throws Exception {
    165. Mac mac = Mac.getInstance("HmacSHA256");
    166. SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
    167. mac.init(secretKeySpec);
    168. return mac.doFinal(msg.getBytes(UTF8));
    169. }
    170. private static String sha256Hex(String s) throws Exception {
    171. MessageDigest md = MessageDigest.getInstance("SHA-256");
    172. byte[] d = md.digest(s.getBytes(UTF8));
    173. return DatatypeConverter.printHexBinary(d).toLowerCase();
    174. }
    175. }

    最关键的就是忽略https验证,建议收藏!!

    1. import cn.hutool.http.HttpRequest;
    2. import cn.hutool.http.Method;
    3. import okhttp3.OkHttpClient;
    4. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    5. import org.apache.http.impl.client.CloseableHttpClient;
    6. import org.apache.http.impl.client.HttpClientBuilder;
    7. import org.apache.http.impl.client.HttpClients;
    8. import org.apache.http.util.TextUtils;
    9. import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
    10. import org.springframework.http.converter.StringHttpMessageConverter;
    11. import org.springframework.web.client.RestTemplate;
    12. import javax.net.ssl.*;
    13. import java.nio.charset.StandardCharsets;
    14. import java.security.cert.CertificateException;
    15. import java.security.cert.X509Certificate;
    16. import java.util.Arrays;
    17. import java.util.concurrent.TimeUnit;
    18. @SuppressWarnings({"Convert2Lambda"})
    19. public class SslUtils {
    20. private static void trustAllHttpsCertificates() throws Exception {
    21. javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
    22. javax.net.ssl.TrustManager tm = new miTM();
    23. trustAllCerts[0] = tm;
    24. SSLContext sc = SSLContext.getInstance("SSL");
    25. sc.init(null, trustAllCerts, null);
    26. javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    27. }
    28. @SuppressWarnings({"unused", "RedundantThrows"})
    29. static class miTM implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {
    30. public X509Certificate[] getAcceptedIssuers() {
    31. return null;
    32. }
    33. public boolean isServerTrusted(X509Certificate[] certs) {
    34. return true;
    35. }
    36. public boolean isClientTrusted(X509Certificate[] certs) {
    37. return true;
    38. }
    39. public void checkServerTrusted(X509Certificate[] certs, String authType)
    40. throws java.security.cert.CertificateException {
    41. }
    42. public void checkClientTrusted(X509Certificate[] certs, String authType)
    43. throws java.security.cert.CertificateException {
    44. }
    45. }
    46. /**
    47. * 忽略SSL证书校验
    48. * @throws Exception e
    49. */
    50. public static void ignoreSsl() throws Exception {
    51. javax.net.ssl.HostnameVerifier hv = new javax.net.ssl.HostnameVerifier() {
    52. public boolean verify(String urlHostName, javax.net.ssl.SSLSession session) {
    53. return true;
    54. }
    55. };
    56. trustAllHttpsCertificates();
    57. javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(hv);
    58. }
    59. /**
    60. * Created with IDEA
    61. * Author: www.itze.cn
    62. * Date: 2021-02-24
    63. * Email:gitlab@111.com
    64. * okhttp忽略所有SSL证书认证
    65. *
    66. * @return
    67. */
    68. public OkHttpClient getUnsafeOkHttpClient() {
    69. try {
    70. final TrustManager[] trustAllCerts = new TrustManager[]{
    71. new X509TrustManager() {
    72. @Override
    73. public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    74. }
    75. @Override
    76. public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    77. }
    78. @Override
    79. public java.security.cert.X509Certificate[] getAcceptedIssuers() {
    80. return new java.security.cert.X509Certificate[]{};
    81. }
    82. }
    83. };
    84. final SSLContext sslContext = SSLContext.getInstance("SSL");
    85. sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
    86. final javax.net.ssl.SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
    87. OkHttpClient.Builder builder = new OkHttpClient.Builder();
    88. builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) (trustAllCerts[0]));
    89. builder.hostnameVerifier(new HostnameVerifier() {
    90. //这里存放不需要忽略SSL证书的域名,为空即忽略所有证书
    91. String[] ssls = {};
    92. @Override
    93. public boolean verify(String hostname, SSLSession session) {
    94. if (TextUtils.isEmpty(hostname)) {
    95. return false;
    96. }
    97. return !Arrays.asList(ssls).contains(hostname);
    98. }
    99. });
    100. OkHttpClient okHttpClient = builder.connectTimeout(10, TimeUnit.MINUTES).
    101. writeTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES).retryOnConnectionFailure(true).build();
    102. return okHttpClient;
    103. } catch (Exception e) {
    104. throw new RuntimeException(e);
    105. }
    106. }
    107. /**
    108. * 跳过证书效验的sslcontext
    109. *
    110. * @return
    111. * @throws Exception
    112. */
    113. private static SSLContext createIgnoreVerifySSL() throws Exception {
    114. SSLContext sc = SSLContext.getInstance("TLS");
    115. // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法
    116. X509TrustManager trustManager = new X509TrustManager() {
    117. @Override
    118. public void checkClientTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
    119. String paramString) throws CertificateException {
    120. }
    121. @Override
    122. public void checkServerTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
    123. String paramString) throws CertificateException {
    124. }
    125. @Override
    126. public java.security.cert.X509Certificate[] getAcceptedIssuers() {
    127. return null;
    128. }
    129. };
    130. sc.init(null, new TrustManager[] { trustManager }, null);
    131. return sc;
    132. }
    133. /**
    134. * 构造RestTemplate
    135. *
    136. * @return
    137. * @throws Exception
    138. */
    139. public static RestTemplate getIgnoreSslRestTemplate() throws Exception {
    140. HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    141. // 超时
    142. factory.setConnectionRequestTimeout(5000);
    143. factory.setConnectTimeout(5000);
    144. factory.setReadTimeout(5000);
    145. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(createIgnoreVerifySSL(),
    146. // 指定TLS版本
    147. null,
    148. // 指定算法
    149. null,
    150. // 取消域名验证
    151. new HostnameVerifier() {
    152. @Override
    153. public boolean verify(String string, SSLSession ssls) {
    154. return true;
    155. }
    156. });
    157. CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
    158. factory.setHttpClient(httpClient);
    159. RestTemplate restTemplate = new RestTemplate(factory);
    160. // 解决中文乱码问题
    161. restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
    162. return restTemplate;
    163. }
    164. /**
    165. * 构造HttpClientBuilder
    166. *
    167. * @return
    168. * @throws Exception
    169. */
    170. public static HttpClientBuilder getIgnoreSslHttpClientBuilder() throws Exception {
    171. HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    172. // 超时
    173. factory.setConnectionRequestTimeout(5000);
    174. factory.setConnectTimeout(5000);
    175. factory.setReadTimeout(5000);
    176. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(createIgnoreVerifySSL(),
    177. // 指定TLS版本
    178. null,
    179. // 指定算法
    180. null,
    181. // 取消域名验证
    182. new HostnameVerifier() {
    183. @Override
    184. public boolean verify(String string, SSLSession ssls) {
    185. return true;
    186. }
    187. });
    188. HttpClientBuilder httpClientBuilder = HttpClients.custom().setSSLSocketFactory(sslsf);
    189. return httpClientBuilder;
    190. }
    191. /**
    192. * 构造HttpRequest hu
    193. *
    194. * @return
    195. * @throws Exception
    196. */
    197. public static HttpRequest getIgnoreSslHttpRequest(String url,Method cMethod) throws Exception {
    198. HttpRequest tHttpRequest = new HttpRequest(url);
    199. tHttpRequest.setMethod(cMethod);
    200. return tHttpRequest.setSSLSocketFactory(createIgnoreVerifySSL().getSocketFactory());
    201. }
    202. }

    四、总结

            经过验证,该方式可以访问经过Nginx代理的腾讯云接口。整个解决过程缺少对问题现状的分析,并没有制定切入点,而是想到哪里改哪里,所以修改的过程异常煎熬。

            后续对于问题的挖掘及解决要整体分析然后列出各个怀疑的情况和解决方案,然后对照着清单逐一排查,如此条理清晰的处理过程才会更有效的解决问题。

    2023-12-12 更新:SslUtils工具类增加多种跳过SSL证书验证逻辑

  • 相关阅读:
    iOS开发:多线程之理解任务和队列
    C#入门(11):泛型介绍
    实验室LIMS管理系统能够解决那些企业难题
    RFSoC应用笔记 - RF数据转换器 -08- RFSoC关键配置之RF-DAC内部解析(二)
    替代MySQL半同步复制,Meta技术团队推出MySQL Raft共识引擎
    C++ Reference: Standard C++ Library reference: C Library: cwchar: vwprintf
    CUDA优化reduce
    ES中的三种查询
    C#列表List的创建与使用
    交换机基本配置(switch)--华为-思科
  • 原文地址:https://blog.csdn.net/AikesLs/article/details/134080623