1.情景展示
通过读取身份证照片上的信息,实现自动填充功能。
2.原因分析
想要解析照片上所携带的相关信息,就需要识别照片的功能,腾讯提供了免费的身份证OCR接口,可供大家使用。
没有耐心的可以直接看接口调用(跳过接口规则介绍)
3.接口规则
接口地址:https://api.ai.qq.com/fcgi-bin/ocr/ocr_idcardocr
API地址:https://ai.qq.com/doc/ocridcardocr.shtml
规则
注意:
1.调取接口的请求方式必须使用form表单提交,JSON请求调取接口无效(我已经试过);
2.返回的是JSON格式的字符串,解析具体数据时,并不能直接当做json对象取值,需要先将其转换成JSON对象。
入参介绍
app_id,注册账号后自动生成的唯一标识,配合密钥,才能拥有调取接口的权限;
time_stamp,时间戳,获取系统当前时间戳(单位是秒,不是毫秒),用于超时校验的(5分钟);
nonce_str,随机字符串,最大长度为32位,我们直接生成32位即可。
sign,用于安全校验,主要是防止数据在传输过程中被篡改的;
image,由图片转换成base64码,只支持jpg,png和bmp格式的图片并且图片大小不能够超过1MB。
另外,不能包含图片类型的声明,即:需要将头部的,"data:image/jpeg;base64,","data:image/png;base64,","data:image/bmp;base64,"去掉。
card_type,就很简单了,如果是正面用0,反面用1。
需要说明的是:app_id,time_stamp和card_type这三个字段的值实际上是字符串,不用理会对于整数的说明,本身它对值的类型根本就限制不住,因为它要求的是必须是form表单提交,form表单提交的数据类型,永远只有一种,那就是字符串,这一点他们是自相矛盾的!
sign的生成规则如下:
意思是说:将这几个必要参数,排除sign后,按照字典升序排列(ASCII升序排列),组成get请求url参数拼接的形式,
1.必要参数的升序拼接结果:app_id=value1&card_type=value2&image=value3&nonce_str=value4&sign=value5&time_stamp=value6
2.image的值,即base64格式的数据需要使用URL进行编码,其余字段的值不需要使用URL编码。
3.生成未加密前的字符串:将第一步的结果在其末尾拼接上密钥,即:app_id=value1&card_type=value2&image=value3&nonce_str=value4&sign=value5&time_stamp=value6&app_key=value7
4.生成sign:对第三步的结果,使用MD5加密,加密结果转大写,得到sign的值。
返回数据
并发量限制(QPS-每秒可调用请求的次数)
4.具体调用
前提:假设前端给我们传过来的是一个base64格式的图片。
设置好常量
// 接口分配的ID private final static int TX_APP_ID = 1111111111; // 接口对应的密钥 private final static String TX_APP_KEY = "2222222222222222"; // 接口调取地址 private final static String TX_ORCURL = "https://api.ai.qq.com/fcgi-bin/ocr/ocr_idcardocr";
导入
import java.util.Map; import java.util.TreeMap; import java.util.UUID;
具体代码
String imgBase64 = request.getParameter("imgBase64"); if (imgBase64.startsWith("data:image/jpeg;base64,")) { imgBase64 = imgBase64.replace("data:image/jpeg;base64,", ""); } else if (imgBase64.startsWith("data:image/png;base64,")) { imgBase64 = imgBase64.replace("data:image/png;base64,", ""); } else if (imgBase64.startsWith("data:image/bmp;base64,")) { imgBase64 = imgBase64.replace("data:image/bmp;base64,", ""); } // 时间戳 int time_stamp = (int) (System.currentTimeMillis()/1000); // 随机字符串 String nonce_str = UUID.randomUUID().toString().replace("-", ""); // 鉴权 String sign = ""; // 1.String image = URLEncoder.encode(imgBase64, "UTF-8"); // base64图片 String image = imgBase64; // 身份证正反面(0-正面;1-反面) int card_type = 0; // 必须使用TreeMap(会自动生成有序Map) Map<String, String> sortedMap = new TreeMap<>(); sortedMap.put("app_id", String.valueOf(TX_APP_ID)); sortedMap.put("time_stamp", String.valueOf(time_stamp)); sortedMap.put("nonce_str", nonce_str); sortedMap.put("image", image); sortedMap.put("card_type", String.valueOf(card_type)); // 生成鉴权 sign = generateSign(sortedMap); sortedMap.put("sign", sign); // 调取腾讯接口 String resultData = httpPostWithForm(TX_ORCURL,sortedMap); // TODO 对resultData进行进一步处理(可直接返给前端处理)
生成鉴权代码
需导入
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Iterator; import java.util.Map; import org.apache.commons.codec.digest.DigestUtils;
/** * 生成腾讯身份证OCR鉴权 * @explain key1=value1&key2=value2&... * @param sortedMap key按照字典进行升序排列的Map * @return sign(MD5加密并转大写) */ private static String generateSign(Map<String, String> sortedMap) { // 鉴权 String sign = ""; Iterator<String> iter = sortedMap.keySet().iterator(); StringBuilder sb = new StringBuilder(); while(iter.hasNext()){ String key =iter.next(); Object value = sortedMap.get(key); // 2.base64需要使用URL进行编码 if (key.equals("image")) { try { value = URLEncoder.encode(sortedMap.get(key), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } sb.append(key).append("=").append(value).append("&"); } sb.append("app_key").append("=").append(TX_APP_KEY); // 加密前 sign = sb.toString(); // 加密后(MD5加密并转大写) sign = DigestUtils.md5Hex(sign).toUpperCase(); return sign; }
发送post请求代码
需导入
import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.commons.httpclient.HttpStatus; import org.apache.http.HttpResponse; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils;
/** * 以form表单形式提交数据,发送post请求 * @explain * 1.请求头:httppost.setHeader("Content-Type","application/x-www-form-urlencoded") * 2.提交的数据格式:key1=value1&key2=value2... * @param url 请求地址 * @param paramsMap 具体数据 * @return 服务器返回数据 */ public static String httpPostWithForm(String url,Map<String, String> paramsMap){ // 用于接收返回的结果 String resultData =""; try { HttpPost post = new HttpPost(url); List<BasicNameValuePair> pairList = new ArrayList<BasicNameValuePair>(); // 迭代Map-->取出key,value放到BasicNameValuePair对象中-->添加到list中 for (String key : paramsMap.keySet()) { pairList.add(new BasicNameValuePair(key, paramsMap.get(key))); } UrlEncodedFormEntity uefe = new UrlEncodedFormEntity(pairList, "utf-8"); post.setEntity(uefe); // 创建一个http客户端 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 发送post请求 HttpResponse response = httpClient.execute(post); // 状态码为:200 if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){ // 返回数据: resultData = EntityUtils.toString(response.getEntity(),"UTF-8"); }else{ throw new RuntimeException("接口连接失败!"); } } catch (Exception e) { throw new RuntimeException("接口连接失败!"); } return resultData; }
说明:
如果我在拼接sign钱前,对image进行url编码,即取消1.的注释,将2.代码注销,将会发生有趣的事情:
调用接口会报错:16388,即鉴权失败,也就是咱们这边生成的sign和腾讯那边生成的sign不一致。
但是,我已经测过了,两种方式,只是对image进行url编码的时间不同而已,并没有出现url编码后image的值不一致的情况,很是奇怪,感兴趣的可以自己试一下。
我这里直接上的是正确的代码,会报异常的代码已经注销。
刚开始的时候,一直卡在这里,错误代码是16388,即:请求签名无效,郁闷了很久,换了一种方式居然可以了,服气!
5.错误代码
具体错误,见:https://ai.qq.com/doc/returncode.shtml
需要注意的一个错误代码是:16432,其实际意思是,你上传的图片不是身份证
但是,错误提示信息却是:
驴头不对马嘴,真是服了!!!
另外,这是图片识别技术,并没有对身份证的真实有效性进行校验,不要想太多!
我们知道,现在手机拍照的照片大小一般在10MB左右,由于腾讯接口只允许接受1MB以内的图片,那我们就必须对图片进行压缩。
以下两种方式任你选。