最近一个微信端项目的功能涉及一个手机拍照上传身份证识别身份证信息回显的功能,调用的接口是腾讯云OCR的卡证识别功能。看了腾讯云的api通俗易懂,本地写好了demo测试可用。H5界面手机拍照调用后台方法保存身份证照片进行识别也可用,难度就在于后台Controller需要再去调用一层接口,在接口实现图片的下载,返回图片下载的物理路径和真实路径。由于后台Controller接收页面的文件类型为MultipartFile类型,亲测使用了三种方法进行传输。只有第三种在测试和线上是可以的。
第一种是将MultipartFile类型转成Base64编码,将Base64编码放进map进行http的dopost传输到接口,接口接收Base64编码,将Base64编码再转成MultipartFile进行下载。以下代码为相关关键代码,具体完整代码例子在第三种方法列出。后台Controller方法:
/**
* 下载图片到接口
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public String ocrImgUpload(HttpServletRequest request, HttpServletResponse response, @RequestParam("file") MultipartFile mfile){
try {
logger.info(new Date() + "=============================下载身份证图片================================");
BASE64Encoder bEncoder = new BASE64Encoder();
String[] suffixArra= mfile.getOriginalFilename().split("\\.");
String preffix="data:image/jpg;base64,".replace("jpg", suffixArra[suffixArra.length - 1]);
String base64EncoderImg = preffix + bEncoder.encode(mfile.getBytes()).replaceAll("[\\s*\t\n\r]", "");
Map<String,String> pm = new HashMap<String,String>();
pm.put("base64EncoderImg", base64EncoderImg);
String res = HttpRequestProxy.doPost(CommonUtils.getRandomAddr(itfUrl)+"/web/file/ocrImgUpload", pm);
Map resMap = WebUtils.getResMap(res);
String rs = (String)resMap.get("rs");
return WebUtils.getData(true, "下载成功", JSONObject.parse(rs));
}catch (Exception e){
logger.error("TencentApiOcrController.java-ocrImgUpload-Exception: ", e);
return WebUtils.resultData("-9999","上传失败",null);
}
}
接口方法:
/**
* 接口接收图片
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public void ocrImgUpload(HttpServletRequest request, HttpServletResponse response){
Long begintime = (new Date()).getTime();
log.info("--------------ocrImgUpload------"+ "begintime========================"+begintime);
PrintWriter out = null;
String res = "";
try{
response.setContentType("text/html; charset=" + jsonCode);
out = response.getWriter();
String base64EncoderImg = request.getParameter("base64EncoderImg")==null?"":request.getParameter("base64EncoderImg").trim();
MultipartFile multipartFile = BASE64DecodedMultipartFile.base64ToMultipart(base64EncoderImg);
Map<String, String> map = new HashMap<String, String>();
map = imgUpload(multipartFile);
res = WebUtils.getData(true,"下载成功",map);
}catch (Exception e) {
e.printStackTrace();
res = WebUtils.getDataByCodeAndMsg(OCREnum.OCR_CHECK_WARN.getCode().toString(), OCREnum.OCR_CHECK_WARN.getMsg());
} finally {
log.info("--------------ocrImgUpload------"+"耗时========================"+(new Date().getTime()-begintime) +"毫秒");
out.write(res);
out.flush();
out.close();
}
}
第二种是将MultipartFile类型转成File类型,将File类型放进Json传输,接口接收File类型文件,再将File类型转成MultipartFile类型进行下载,以下代码为相关关键代码,具体完整代码例子在第三种方法列出。
/**
* 后台web端Controller
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public String ocrImgUpload(HttpServletRequest request, HttpServletResponse response, @RequestParam("file") MultipartFile mfile){
try {
logger.info("=============================下载身份证图片================================");
File file = new File(mfile.getOriginalFilename());
FileUtils.copyInputStreamToFile(mfile.getInputStream(),file);
String multfile = JSON.toJSONString(file);
Map<String,String> pm = new HashMap<String,String>();
pm.put("multfile", multfile);
String res = HttpRequestProxy.doPost(CommonUtils.getRandomAddr(itfUrl)+"/web/file/ocrImgUpload", pm);
Map resMap = WebUtils.getResMap(res);
String rs = (String)resMap.get("rs");
return WebUtils.getData(true, "下载成功", JSONObject.parse(rs));
}catch (Exception e){
logger.error("TencentApiOcrController.java-ocrImgUpload-Exception: ", e);
return WebUtils.getData(false,"下载失败",null);
}
}
/**
* 接口方法
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public void ocrImgUpload(HttpServletRequest request, HttpServletResponse response){
Long begintime = (new Date()).getTime();
log.info("--------------ocrImgUpload------"+ "begintime========================"+begintime);
PrintWriter out = null;
String res = "";
try{
response.setContentType("text/html; charset=" + jsonCode);
out = response.getWriter();
String multfile = request.getParameter("multfile")==null?"":request.getParameter("multfile").trim();
String file = StringEscapeUtils.unescapeHtml(multfile);
File newFile = JSONObject.parseObject(file, File.class);
FileInputStream input = new FileInputStream(newFile);
MultipartFile multipartFile = new MockMultipartFile("file", newFile.getName(), "text/plain", IOUtils.toByteArray(input));
Map<String, String> map = new HashMap<String, String>();
map = imgUpload(multipartFile);
res = WebUtils.getData(true,"下载成功",map);
}catch (Exception e) {
e.printStackTrace();
res = WebUtils.getDataByCodeAndMsg(OCREnum.OCR_CHECK_WARN.getCode().toString(), OCREnum.OCR_CHECK_WARN.getMsg());
} finally {
log.info("--------------ocrImgUpload------"+"耗时========================"+(new Date().getTime()-begintime) +"毫秒");
out.write(res);
out.flush();
out.close();
}
}
以上两种方法虽然都能成功下载身份证,但是在测试环境和线上环境却报图片不存在,因为这两种方法在将图片类型进行转换的时候,使用FileInputStream流输出,并且在本地项目下生成了新图片,但是此时我还未调用到图片下载的方法,即未将图片下载到服务器上,我本地使用的是tomcat。所以造成了图片未找到的报错。由于我是本地环境所以不存在图片未找到。以下给出项目完整代码。项目需要使用到的依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.19</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.49</version>
</dependency>
前端H5界面代码(以下代码给出识别正面身份证代码,反面同正面一样,只需要将FRONT改为BACK即可,此处可查看腾讯云OCR识别的API参数传参):
//设置隐藏域接收图片地址
<input type="hidden" id="filePath1" name="filePath1"/>
<input type="hidden" id="filePath2" name="filePath2"/>
<input type="hidden" id="urlPath1" name="urlPath1"/>
<input type="hidden" id="urlPath2" name="urlPath2"/>
//拍照按钮
<div class="id_img_wp">
<input type="file" accept="image/*" capture='camera' id="front_z" style="display: none" name="file"/>
<div class="img_wp" onclick="frontclick()">
<img th:src="@{/ui/img/front.png}" id="front" class="img_card"/>
<p class="img_intro">人像面</p>
</div>
<input type="file" accept="image/*" capture='camera' id="back_z" style="display: none" name="file"/>
<div class="img_wp" onclick="backclick()">
<img th:src="@{/ui/img/back.png}" id="back" class="img_card"/>
<p class="img_intro">国徽面</p>
</div>
</div>
//正面
function frontclick() {
$('#front_z').click();
}
//反面
function backclick() {
if($("#filePath1").val() == null || $("#filePath1").val() == "" || $("#filePath1").val() == undefined){
layer.msg("请先拍照身份证人像面",{time:1000});
return;
}
$('#back_z').click();
}
//这里使用layui上传组件,引入layui的js和css即可
<script>
layui.use('upload', function () {
var $ = layui.jquery
, upload = layui.upload;
//普通图片上传
var uploadInst = upload.render({
elem: '#front_z',
url: pcs.common.path + '/web/ocr/ocrImgUpload'
,async: false
, before: function (obj) {
//预读本地文件示例,不支持ie8
obj.preview(function (index, file, result) {
$('#zmz').attr('src', result); //图片链接(base64)
});
}
, done: function (res, upload) {//上传完毕回调
if (res.code != 1) {
return layer.msg('上传失败',{time:1000});
} else {
var picUrl = res.rs.picUrl;
var picPath = res.rs.picPath;
$('#front').attr('src', picUrl);
$("#filePath1").val(picPath);
$("#urlPath1").val(picUrl);
//调用后台腾讯云OCR认证识别
if ($("#filePath1").val() != null && $("#filePath1").val() != undefined && $("#filePath1").val() != "") {
frontOcr($("#filePath1").val(), "FRONT");
}
}
}
, error: function () {
//演示失败状态,并实现重传
var demoText = $('#demoText');
demoText.html('<span style="color: #FF5722;">上传失败</span> <a class="layui-btn layui-btn-xs demo-reload">重试</a>');
demoText.find('.demo-reload').on('click', function () {
uploadInst.upload();
});
}
});
});
</script>
//上传完毕后调用frontOcr()方法,传入图片物理路径调用后台方法进行识别回显
//正面调用腾讯云OCR认证
function frontOcr(imageStr, idCardSide) {
$.ajax({
url: pcs.common.path + "/web/ocr/getIdcardOcr",
data: {
imageStr: imageStr,
idCardSide: idCardSide
},
type: 'post',
dataType: 'json',
success: function (data) {
if (data.success) {
var cardInfo = data.rs;
var Address = cardInfo.Address;
var Birth = cardInfo.Birth;
var IdNum = cardInfo.IdNum;
var Name = cardInfo.Name;
var Nation = cardInfo.Nation;
var Sex = cardInfo.Sex;
var tempId = cardInfo.AdvancedInfo;
var obj = JSON.parse(tempId);
var arr = obj.WarnInfos;
//以下值都为页面表单字段 自行添加即可
$("#Name").val(Name);
$("#Sex").val(Sex);
$("#IdNum").val(IdNum);
$("#Address").val(Address);
$("#tempId").val(arr);
$("#Birth").val(Birth);
} else {
layer.msg(data.errmsg,{time:1000});
}
}
});
}
<input type="file" accept="image/*" capture='camera' id="front_z" style="display: none" name="file"/>这行代码是设置手机拍照功能。
WebController:
/**
* WebController层
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public String ocrImgUpload(HttpServletRequest request, HttpServletResponse response, @RequestParam("file") MultipartFile mfile){
try {
logger.info(new Date() + "=============================下载身份证图片================================");
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile file = multipartRequest.getFile("file");//从request中获取前端穿过来的文件
//调用httpclient
String res = HttpCilentMultipartFile.uploadFile(CommonUtils.getRandomAddr(itfUrl)+"/web/file/ocrImgUpload", file, "file");
Map resMap = WebUtils.getResMap(res);
String rs = (String)resMap.get("rs");
return WebUtils.resultData("1", "上传成功", JSONObject.parse(rs));
}catch (Exception e){
logger.error("TencentApiOcrController.java-ocrImgUpload-Exception: ", e);
return WebUtils.resultData("-9999","上传失败",null);
}
}
HTTP传输工具类:
package com.fjqwkj.commons.utils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
/**
* @Author: fd
* @Date 2020/9/11 10:13
* @Description:
*/
public class HttpCilentMultipartFile {
public static String uploadFile(String requestURL , MultipartFile file, String fileParamName) {
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
try {
String fileName = file.getOriginalFilename();
HttpPost httpPost = new HttpPost(requestURL);
//添加header
// for (Map.Entry<String, String> e : headerParams.entrySet()) {
// httpPost.addHeader(e.getKey(), e.getValue());
// }
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setCharset(Charset.forName("utf-8"));
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//加上此行代码解决返回中文乱码问题
builder.addBinaryBody(fileParamName, file.getInputStream(), ContentType.MULTIPART_FORM_DATA, fileName);// 文件流
/*for (Map.Entry<String, String> e : otherParams.entrySet()) {
builder.addTextBody(e.getKey(), e.getValue());// 类似浏览器表单提交,对应input的name和value
}*/
HttpEntity entity = builder.build();
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);// 执行提交
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
// 将响应内容转换为字符串
result = EntityUtils.toString(responseEntity, Charset.forName("UTF-8"));
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
接口itfController:
/**
* 下载图片到接口
* @return
*/
@RequestMapping(value = "/ocrImgUpload", method = RequestMethod.POST)
public void ocrImgUpload(HttpServletRequest request, HttpServletResponse response,@RequestParam(value = "file") MultipartFile file){
Long begintime = (new Date()).getTime();
log.info("--------------ocrImgUpload------"+ "begintime========================"+begintime);
PrintWriter out = null;
String res = "";
try{
response.setContentType("text/html; charset=" + jsonCode);
out = response.getWriter();
// String base64EncoderImg = request.getParameter("base64EncoderImg")==null?"":request.getParameter("base64EncoderImg").trim();
// MultipartFile multipartFile = BASE64DecodedMultipartFile.base64ToMultipart(base64EncoderImg);
Map<String, String> map = new HashMap<String, String>();
map = imgUpload(file);
res = WebUtils.getData(true,"下载成功",map);
}catch (Exception e) {
e.printStackTrace();
res = WebUtils.getDataByCodeAndMsg(OCREnum.OCR_CHECK_WARN.getCode().toString(), OCREnum.OCR_CHECK_WARN.getMsg());
} finally {
log.info("--------------ocrImgUpload------"+"耗时========================"+(new Date().getTime()-begintime) +"毫秒");
out.write(res);
out.flush();
out.close();
}
}
//图片下载方法
public Map<String, String> imgUpload(MultipartFile file) {
Map<String, String> map = new HashMap<String, String>();
String res = "";
if (!file.isEmpty()) {
String msg = "";
//最大文件大小
long maxSize = 52428800;//50M
//检查目录
File uploadDir = new File(realPath);
log.info("uploadDir" + uploadDir);
if (!uploadDir.isDirectory()) {
msg = "上传目录不存在,请确认您配置的物理路径是否正确!";
map.put("false", msg);
return map;
}
//检查目录写权限
if (!uploadDir.canWrite()) {
msg = "上传目录没有写权限。";
map.put("false", msg);
return map;
}
//创建文件夹-正式
String dirName = "image";
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String ymd = sdf.format(new Date());
sdf = new SimpleDateFormat("hh");
String hh = sdf.format(new Date());
sdf = new SimpleDateFormat("mm");
String mm = sdf.format(new Date());
String realpathStr = realPath + "/" + dirName + "/";
log.info("正式文件夹目录:" + realpathStr);
File realFile = new File(realpathStr);
if (!realFile.exists()) {
realFile.mkdirs();
}
log.info("realFile:"+realFile);
realpathStr += ymd + "/" + ymd + hh + "/" + ymd + hh + mm + "/";
String realUrlStr = realUrl + "/" + dirName + "/" + ymd + "/" + ymd + hh + "/" + ymd + hh + mm + "/";
File realFile2 = new File(realpathStr);
if (!realFile2.exists()) {
realFile2.mkdirs();
}
log.info("realUrlStr:"+realUrlStr);
log.info("realFile2:"+realFile2);
//检查文件大小
if (file.getSize() > maxSize) {
msg = "上传文件太大,不能超过50M";
map.put("false", msg);
return map;
}
String fileName = file.getOriginalFilename();
//检查扩展名
String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
if (!("gif".equals(fileExt) || "jpg".equals(fileExt) || "jpeg".equals(fileExt) || "png".equals(fileExt) || "bmp".equals(fileExt))) {
msg = "上传文件扩展名是不允许的扩展名。只允许gif,jpg,jpeg,png,bmp格式。";
map.put("false", msg);
return map;
}
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
String newFileName = df.format(new Date()) + "_" + new Random().nextInt(1000) + "." + fileExt;
try {
File uploadedFile = new File(realpathStr, newFileName);
file.transferTo(uploadedFile);
} catch (Exception e) {
msg = "上传文件失败。";
map.put("false", msg);
return map;
}
log.info("newFileName:"+newFileName);
String picUrl = realUrlStr + newFileName;
String picPath = realpathStr + newFileName;
log.info("picUrl:"+picUrl);
map.put("picUrl", picUrl);
map.put("picPath", picPath);
return map;
} else {
map.put("false", "上传文件失败");
return map;
}
}
本地项目文件路径配置:
#####文件路径配置
local_url: http://127.0.0.1:8080/file/upload/tem_file/ #临时访问路径
tmp_url: E:/tomcat/apache-tomcat-7.0.94/webapps/file/upload/tem_file/ #临时存放路径
real_path: E:/tomcat/apache-tomcat-7.0.94/webapps/file/upload/real_file/ #真实路径
real_url: http://127.0.0.1:8080/file/upload/real_file/
到此为止才将文件从h5传到webcontroller再http传输到接口进行下载。最后一步就是将返回的图片进行OCR识别回显信息。
/**
* 获取身份证OCR识别信息.
* @return
*/
@RequestMapping(value = "/getIdcardOcr",method = RequestMethod.POST,produces = "text/html;charset=utf-8")
@ResponseBody
public String getIdcardOcr(HttpServletRequest request){
try {
String imageStr = request.getParameter("imageStr")==null?"":request.getParameter("imageStr").trim();
String idCardSide = request.getParameter("idCardSide")==null?"":request.getParameter("idCardSide").trim();
imageStr = StrUtils.xssEncode(imageStr);
idCardSide = StrUtils.xssEncode(idCardSide);
Map<String,String> pm = new HashMap<String,String>();
if(StringUtils.isNotBlank(imageStr)){
pm.put("imageStr", imageStr);
}
if(StringUtils.isNotBlank(idCardSide)){
pm.put("idCardSide", idCardSide);
}
imageStr = PicToBase64.ImageToBase64(imageStr);
String rs = tencentApiOcrService.getIdcardOcr(idCardSide, imageStr);
return WebUtils.getData(true, "OCR识别成功", JSONObject.parse(rs));
} catch (Exception e) {
logger.error("TencentApiOcrController.java-getIdcardOcr-Exception: ", e);
return WebUtils.getData(false,"OCR识别失败",null);
}
}
public interface TencentApiOcrService {
public String getIdcardOcr(String cardSide, String imageStr);
}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fjqwkj.commons.utils.CommonUtils;
import com.fjqwkj.commons.utils.HttpRequestProxy;
import com.fjqwkj.commons.utils.WebUtils;
import com.fjqwkj.service.apiocr.TencentApiOcrService;
import com.fjqwkj.service.checkocr.TCheckOcrService;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.ocr.v20181119.OcrClient;
import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRRequest;
import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: fd
* @Date 2020/9/2 20:48
* @Description:
*/
@Service
public class TencentApiOcrServiceImpl implements TencentApiOcrService {
private static Logger logger = LoggerFactory.getLogger(TencentApiOcrService.class);
private static final String FIELD_SECRET_ID = "";//填写secretId
private static final String FIELD_SECRET_KEY = "";//填写secretKey
private static final String FIELD_TENCENT_CLOUD_API = "ocr.tencentcloudapi.com";
private static final String FIELD_REGION = "ap-guangzhou";
@Value("${itf_url:}")
private String itfUrl;
@Autowired
private TCheckOcrService tCheckOcrService;
/**
* 获取OcrClient.
*
* @return
*/
private OcrClient getOcrClient(){
Credential cred = new Credential(FIELD_SECRET_ID, FIELD_SECRET_KEY);
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(FIELD_TENCENT_CLOUD_API);
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
OcrClient ocrClient = new OcrClient(cred, FIELD_REGION, clientProfile);
return ocrClient;
}
/**
* 身份证识别
*
* @param cardSide
* @param imageStr
* @return
*/
public String getIdcardOcr(String cardSide, String imageStr) {
OcrClient ocrClient = getOcrClient();
Map<String, String> params = new HashMap<>();
params.put("ImageBase64", imageStr);
params.put("CardSide", cardSide);
Map<String, Boolean> configMap = new HashMap<>();
configMap.put("CopyWarn",true);
configMap.put("BorderCheckWarn",true);
configMap.put("ReshootWarn",true);
configMap.put("DetectPsWarn",true);
configMap.put("TempIdWarn",true);
configMap.put("InvalidDateWarn",true);
String Config = JSON.toJSONString(configMap);
params.put("Config", Config);
try {
IDCardOCRRequest req = IDCardOCRRequest.fromJsonString(JSON.toJSONString(params), IDCardOCRRequest.class);
IDCardOCRResponse resp = ocrClient.IDCardOCR(req);
HashMap<String, String> dataMap = new HashMap<>();
resp.toMap(dataMap, "");
return JSONObject.toJSONString(dataMap);
} catch (Exception e) {
logger.info(e.getMessage());
}
return null;
}
}
到这里整个流程就结束,可以实现身份证识别回显。总结一下,如果使用base64传输的话,因为将图片编码成base64位,编码大小会随着图片的大小像素而改变,将普通拍照上传的一张图片进行编码后,查看它的字符多达600多万,而http传输过程String类型有大小限制,会导致接口那边接收到的base64编码是空的,导致无法转成图片进行下载。还有使用base64编码的一个坏处就是容易造成解码错误,与原来图片不匹配。最主要还是生成的base64编码实在太大,过程太慢了。所以最后使用MultipartFile传输是最快最安全的。