fix: 自定义标签

This commit is contained in:
2026-03-03 15:40:21 +08:00
parent fe4e742551
commit 13fe5d9124
41 changed files with 2643 additions and 0 deletions

51
backend-idcard/README.md Normal file
View File

@@ -0,0 +1,51 @@
# 身份证识别(百度 OCR + AES
调用百度身份证 OCR 接口,对图片做 AES 加密上传,对返回的 `result` 密文做 Base64 解码 + AES 解密后解析出所需字段。
## 依赖
- JDK 8+
- [fastjson](https://github.com/alibaba/fastjson)(解析返回 JSON
## 配置
从百度控制台获取:
- **access_token**OAuth2 获取(或使用 API Key + Secret Key 换 token
- **aesKey**16 位 hex 字符串(控制台身份证 OCR 安全设置里)
## 使用示例
```java
// 构造识别器accessToken、aesKey 建议从配置文件或环境变量读取)
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
// 方式一:本地文件
IdcardRecognizer.IdcardResult result = recognizer.recognize("/path/to/idcard_front.jpg", "front");
if (result != null) {
System.out.println("姓名: " + result.getName());
System.out.println("身份证号: " + result.getIdNumber());
System.out.println("出生: " + result.getBirth());
System.out.println("住址: " + result.getAddress());
// ...
}
// 方式二:上传的图片字节(如 MultipartFile.getBytes()
byte[] imgBytes = ...;
IdcardRecognizer.IdcardResult back = recognizer.recognize(imgBytes, "back");
if (back != null) {
System.out.println("签发机关: " + back.getIssueAuthority());
System.out.println("有效期限: " + back.getValidDate());
}
```
## 返回字段说明
- **正面 (side=front)**`name` 姓名、`gender` 性别、`nation` 民族、`birth` 出生、`address` 住址、`idNumber` 公民身份号码
- **反面 (side=back)**`issueAuthority` 签发机关、`validDate` 有效期限
按百度接口约定,请求时 `id_card_side``front``back`
## 集成到现有后端
`com.ydoyun.ocr` 包拷贝到你的项目中,并加入 fastjson 依赖即可;若项目已使用 Spring可把 `IdcardRecognizer` 做成 Bean`accessToken``aesKey``@Value` 或配置类注入。

27
backend-idcard/pom.xml Normal file
View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ydoyun</groupId>
<artifactId>idcard-ocr</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Idcard OCR (Baidu)</name>
<description>百度身份证 OCR 调用AES 加解密)</description>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.43</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,316 @@
package cn.iocoder.yudao.module.car.baidu;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
import java.util.Map;
/**
* 获取 token + 身份证识别(百度 OCRAES 加解密)。
*/
public class IdcardRecognizer {
private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
private final String accessToken;
private byte[] originAesKey;
public IdcardRecognizer(String accessToken, String aesKey) {
this.accessToken = accessToken;
try {
this.originAesKey = AesKeyUtil.parseAesKey(aesKey);
} catch (Exception e) {
throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e);
}
}
// ===================== 获取 token你原来的逻辑 =====================
/**
* 获取权限token
* @return 返回示例:
* {
* "access_token": "24.460da4889caad24cccdb1fea17221975.2592000.1491995545.282335-1234567",
* "expires_in": 2592000
* }
*/
public static String getAuth() {
String clientId = "mYchXvYyrwXbTscZ0HWfR88s";
String clientSecret = "AD4QSVg7OMP0GGkcqxkDSwrn7V7rkShN";
return getAuth(clientId, clientSecret);
}
/**
* 获取API访问token
* @param ak - 百度云官网获取的 API Key
* @param sk - 百度云官网获取的 Securet Key
* @return assess_token
*/
public static String getAuth(String ak, String sk) {
String authHost = "https://aip.baidubce.com/oauth/2.0/token?";
String getAccessTokenUrl = authHost
+ "grant_type=client_credentials"
+ "&client_id=" + ak
+ "&client_secret=" + sk;
try {
URL realUrl = new URL(getAccessTokenUrl);
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
connection.setRequestMethod("GET");
connection.connect();
Map<String, List<String>> map = connection.getHeaderFields();
for (String key : map.keySet()) {
System.err.println(key + "--->" + map.get(key));
}
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String result = "";
String line;
while ((line = in.readLine()) != null) {
result += line;
}
System.err.println("result:" + result);
JSONObject jsonObject = JSONObject.parseObject(result);
String access_token = jsonObject.getString("access_token");
return access_token;
} catch (Exception e) {
System.err.printf("获取token失败");
e.printStackTrace(System.err);
}
return null;
}
// ===================== 身份证识别AES 加解密) =====================
/**
* 识别身份证图片(本地文件路径)
* @param filePath 图片本地路径
* @param side 正面 "front" 或反面 "back"
* @return 解析后的身份证字段,识别失败返回 null
*/
public IdcardResult recognize(String filePath, String side) throws Exception {
byte[] imgData = Files.readAllBytes(Paths.get(filePath));
return recognize(imgData, side);
}
/**
* 识别身份证图片(字节数组,适合上传场景)
* @param imgData 图片字节
* @param side 正面 "front" 或反面 "back"
* @return 解析后的身份证字段,识别失败返回 null
*/
public IdcardResult recognize(byte[] imgData, String side) throws Exception {
String plainJson = recognizePlainJson(imgData, side);
return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side);
}
/**
* 识别并返回解密后的明文 JSON便于调试/落库)。
*/
public String recognizePlainJson(byte[] imgData, String side) throws Exception {
String imgStr = encryptImg(imgData);
String imgParam = URLEncoder.encode(imgStr, "UTF-8");
String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true";
String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body);
if (rawResponse == null || rawResponse.isEmpty()) {
return null;
}
return decryptResult(rawResponse);
}
private String encryptImg(byte[] imgData) throws Exception {
byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey);
return Base64.getEncoder().encodeToString(encImgBytes);
}
private String decryptResult(String encryptResponse) throws Exception {
JSONObject obj = JSON.parseObject(encryptResponse);
String result = obj.getString("result");
if (result == null) return null;
byte[] arr = Base64.getDecoder().decode(result);
return new String(AesUtil.decrypt(arr, originAesKey), "UTF-8");
}
private IdcardResult parseToResult(String plainJson, String side) {
JSONObject root = JSON.parseObject(plainJson);
JSONObject wordsResult = root.getJSONObject("words_result");
if (wordsResult == null) return null;
IdcardResult result = new IdcardResult();
result.setSide("front".equalsIgnoreCase(side) ? "front" : "back");
if ("front".equalsIgnoreCase(side)) {
result.setName(getWords(wordsResult, "姓名"));
result.setGender(getWords(wordsResult, "性别"));
result.setNation(getWords(wordsResult, "民族"));
result.setBirth(getWords(wordsResult, "出生"));
result.setAddress(getWords(wordsResult, "住址"));
result.setIdNumber(getWords(wordsResult, "公民身份号码"));
} else {
result.setIssueAuthority(getWords(wordsResult, "签发机关"));
result.setValidDate(getWords(wordsResult, "有效期限"));
}
return result;
}
private static String getWords(JSONObject wordsResult, String key) {
if (wordsResult == null) return null;
JSONObject item = wordsResult.getJSONObject(key);
return item == null ? null : item.getString("words");
}
// --------------- 结果 DTO ---------------
public static class IdcardResult {
private String side;
private String name;
private String gender;
private String nation;
private String birth;
private String address;
private String idNumber;
private String issueAuthority;
private String validDate;
public String getSide() { return side; }
public void setSide(String side) { this.side = side; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getNation() { return nation; }
public void setNation(String nation) { this.nation = nation; }
public String getBirth() { return birth; }
public void setBirth(String birth) { this.birth = birth; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getIdNumber() { return idNumber; }
public void setIdNumber(String idNumber) { this.idNumber = idNumber; }
public String getIssueAuthority() { return issueAuthority; }
public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; }
public String getValidDate() { return validDate; }
public void setValidDate(String validDate) { this.validDate = validDate; }
}
// --------------- 工具类 ---------------
static class HttpUtil {
static String post(String url, String accessToken, String formBody) throws IOException {
String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(15000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
OutputStream os = conn.getOutputStream();
try {
os.write(bodyBytes);
} finally {
os.close();
}
int code = conn.getResponseCode();
InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
if (is == null) return null;
try {
byte[] bytes = readAllBytes(is);
return new String(bytes, "UTF-8");
} finally {
conn.disconnect();
}
}
private static byte[] readAllBytes(InputStream in) throws IOException {
byte[] buf = new byte[4096];
int n;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((n = in.read(buf)) >= 0) {
if (n > 0) bos.write(buf, 0, n);
}
return bos.toByteArray();
}
}
static class AesKeyUtil {
private static final String HEX = "0123456789abcdef";
static byte[] parseAesKey(String hex) throws Exception {
if (hex == null || hex.length() != 16) {
throw new Exception("aes key 需为 16 位");
}
char[] data = hex.toCharArray();
byte[] out = new byte[data.length];
for (int i = 0; i < data.length; i++) {
int f = HEX.indexOf(data[i]);
if (f < 0) throw new Exception("aes key 需为 hex 字符");
out[i] = (byte) f;
}
return out;
}
}
static class AesUtil {
private static final String ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception {
Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE);
return cipher.doFinal(src);
}
static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception {
Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE);
return cipher.doFinal(src);
}
private static Cipher getCipher(byte[] aesKey, int mode) throws Exception {
SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
cipher.init(mode, spec);
return cipher;
}
}
// --------------- 测试 main改下面三个变量后直接运行 ---------------
public static void main(String[] args) throws Exception {
// 1. 用你原来的 getAuth 拿 token
String accessToken = getAuth();
if (accessToken == null) {
System.out.println("获取 access_token 失败");
return;
}
// 2. 从控制台拿 16 位 aesKeyhex 字符串)
String aesKey = "填写16位aesKey";
String filePath = "填写身份证图片路径";
String side = "front"; // front 正面 / back 反面
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
IdcardResult result = recognizer.recognize(filePath, side);
System.out.println("========== 识别结果 ==========");
System.out.println(JSON.toJSONString(result, true));
}
}

View File

@@ -0,0 +1,254 @@
package com.ydoyun.ocr;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* 身份证识别器:调用百度 OCR 身份证接口AES 加密传输),解析并返回所需字段。
* 需配置accessToken、aesKey16 位 hex可从百度控制台获取。
*/
public class IdcardRecognizer {
private static final String IDCARD_OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
private final String accessToken;
private byte[] originAesKey;
public IdcardRecognizer(String accessToken, String aesKey) {
this.accessToken = accessToken;
try {
this.originAesKey = AesKeyUtil.parseAesKey(aesKey);
} catch (Exception e) {
throw new IllegalArgumentException("aesKey 非法,需 16 位 hex", e);
}
}
/**
* 识别身份证图片(本地文件路径)
*
* @param filePath 图片本地路径
* @param side 正面 "front" 或反面 "back"
* @return 解析后的身份证字段,识别失败返回 null
*/
public IdcardResult recognize(String filePath, String side) throws IOException, Exception {
byte[] imgData = Files.readAllBytes(Paths.get(filePath));
return recognize(imgData, side);
}
/**
* 识别身份证图片(字节数组,适合上传场景)
*
* @param imgData 图片字节
* @param side 正面 "front" 或反面 "back"
* @return 解析后的身份证字段,识别失败返回 null
*/
public IdcardResult recognize(byte[] imgData, String side) throws Exception {
String plainJson = recognizePlainJson(imgData, side);
return (plainJson == null || plainJson.isEmpty()) ? null : parseToResult(plainJson, side);
}
/**
* 识别并返回解密后的明文 JSON便于调试/落库/自行解析)。
*/
public String recognizePlainJson(byte[] imgData, String side) throws Exception {
String imgStr = encryptImg(imgData);
String imgParam = URLEncoder.encode(imgStr, StandardCharsets.UTF_8.name());
String body = "id_card_side=" + side + "&image=" + imgParam + "&AESEncry=true";
String rawResponse = HttpUtil.post(IDCARD_OCR_URL, accessToken, body);
if (rawResponse == null || rawResponse.isEmpty()) {
return null;
}
return decryptResult(rawResponse);
}
private String encryptImg(byte[] imgData) throws Exception {
byte[] encImgBytes = AesUtil.encrypt(imgData, originAesKey);
return Base64.getEncoder().encodeToString(encImgBytes);
}
private String decryptResult(String encryptResponse) throws Exception {
JSONObject obj = JSON.parseObject(encryptResponse);
String result = obj.getString("result");
if (result == null) return null;
byte[] arr = Base64.getDecoder().decode(result);
return new String(AesUtil.decrypt(arr, originAesKey), StandardCharsets.UTF_8);
}
private IdcardResult parseToResult(String plainJson, String side) {
JSONObject root = JSON.parseObject(plainJson);
JSONObject wordsResult = root.getJSONObject("words_result");
if (wordsResult == null) return null;
IdcardResult result = new IdcardResult();
result.setSide("front".equalsIgnoreCase(side) ? "front" : "back");
if ("front".equalsIgnoreCase(side)) {
result.setName(getWords(wordsResult, "姓名"));
result.setGender(getWords(wordsResult, "性别"));
result.setNation(getWords(wordsResult, "民族"));
result.setBirth(getWords(wordsResult, "出生"));
result.setAddress(getWords(wordsResult, "住址"));
result.setIdNumber(getWords(wordsResult, "公民身份号码"));
} else {
result.setIssueAuthority(getWords(wordsResult, "签发机关"));
result.setValidDate(getWords(wordsResult, "有效期限"));
}
return result;
}
private static String getWords(JSONObject wordsResult, String key) {
if (wordsResult == null) return null;
JSONObject item = wordsResult.getJSONObject(key);
return item == null ? null : item.getString("words");
}
// --------------- 结果 DTO ---------------
public static class IdcardResult {
private String side;
private String name;
private String gender;
private String nation;
private String birth;
private String address;
private String idNumber;
private String issueAuthority;
private String validDate;
public String getSide() { return side; }
public void setSide(String side) { this.side = side; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
public String getNation() { return nation; }
public void setNation(String nation) { this.nation = nation; }
public String getBirth() { return birth; }
public void setBirth(String birth) { this.birth = birth; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getIdNumber() { return idNumber; }
public void setIdNumber(String idNumber) { this.idNumber = idNumber; }
public String getIssueAuthority() { return issueAuthority; }
public void setIssueAuthority(String issueAuthority) { this.issueAuthority = issueAuthority; }
public String getValidDate() { return validDate; }
public void setValidDate(String validDate) { this.validDate = validDate; }
}
// --------------- 工具类 ---------------
static class HttpUtil {
static String post(String url, String accessToken, String formBody) throws IOException {
String fullUrl = url + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
byte[] bodyBytes = formBody.getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(15_000);
conn.setReadTimeout(30_000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
try (OutputStream os = conn.getOutputStream()) {
os.write(bodyBytes);
}
int code = conn.getResponseCode();
InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
if (is == null) return null;
try (InputStream in = is) {
byte[] bytes = readAllBytes(in);
return new String(bytes, StandardCharsets.UTF_8);
} finally {
conn.disconnect();
}
}
private static byte[] readAllBytes(InputStream in) throws IOException {
byte[] buf = new byte[4096];
int n;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((n = in.read(buf)) >= 0) {
if (n > 0) bos.write(buf, 0, n);
}
return bos.toByteArray();
}
}
static class AesKeyUtil {
private static final String HEX = "0123456789abcdef";
static byte[] parseAesKey(String hex) throws Exception {
if (hex == null || hex.length() != 16) {
throw new Exception("aes key 需为 16 位");
}
char[] data = hex.toCharArray();
byte[] out = new byte[data.length];
for (int i = 0; i < data.length; i++) {
int f = HEX.indexOf(data[i]);
if (f < 0) throw new Exception("aes key 需为 hex 字符");
out[i] = (byte) f;
}
return out;
}
}
static class AesUtil {
private static final String ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/ECB/PKCS5Padding";
static byte[] encrypt(byte[] src, byte[] aesKey) throws Exception {
Cipher cipher = getCipher(aesKey, Cipher.ENCRYPT_MODE);
return cipher.doFinal(src);
}
static byte[] decrypt(byte[] src, byte[] aesKey) throws Exception {
Cipher cipher = getCipher(aesKey, Cipher.DECRYPT_MODE);
return cipher.doFinal(src);
}
private static Cipher getCipher(byte[] aesKey, int mode) throws Exception {
SecretKeySpec spec = new SecretKeySpec(aesKey, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
cipher.init(mode, spec);
return cipher;
}
}
/**
* 本地快速测试入口JDK8 可运行,直接改下面四个变量再运行)。
*/
public static void main(String[] args) throws Exception {
// TODO替换成你自己的配置
String accessToken = "填写你的 access_token";
String aesKey = "填写 16 位 aesKeyhex 字符串)";
String side = "front"; // front 正面 / back 反面
String filePath = "填写身份证图片本地路径";
IdcardRecognizer recognizer = new IdcardRecognizer(accessToken, aesKey);
IdcardResult result = recognizer.recognize(filePath, side);
System.out.println(JSON.toJSONString(result, true));
// 如果你要看解密后的明文 JSON排查字段/定位问题),放开下面三行:
// byte[] img = Files.readAllBytes(Paths.get(filePath));
// String plain = recognizer.recognizePlainJson(img, side);
// System.out.println(plain);
}
}

Binary file not shown.

View File

@@ -0,0 +1,3 @@
artifactId=idcard-ocr
groupId=com.ydoyun
version=1.0.0

View File

@@ -0,0 +1,5 @@
com/ydoyun/ocr/IdcardRecognizer$HttpUtil.class
com/ydoyun/ocr/IdcardRecognizer$AesKeyUtil.class
com/ydoyun/ocr/IdcardRecognizer$IdcardResult.class
com/ydoyun/ocr/IdcardRecognizer$AesUtil.class
com/ydoyun/ocr/IdcardRecognizer.class

View File

@@ -0,0 +1 @@
/Users/ouhaolan/project/百盛科技/报表/yudao-ui-admin-vue3/backend-idcard/src/main/java/com/ydoyun/ocr/IdcardRecognizer.java