完成实名认证的功能定义,使用数据宝的二元实名认证接口(仅验证人名、身份证)

This commit is contained in:
jkcdev 2025-05-29 18:16:41 +08:00
parent 888374a84f
commit 7abbe96a36
12 changed files with 318 additions and 123 deletions

View File

@ -180,6 +180,14 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- 数据宝实名认证加密 -->
<dependency>
<groupId>com.cdp.product</groupId>
<artifactId>cdp-common-security</artifactId>
<version>11.0.0-SNAPSHOT</version>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>
<!--开发环境--> <!--开发环境-->

View File

@ -0,0 +1,34 @@
package com.cdzy.orders.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 实名认证配置类
* 使用数据宝进行实名认证
*
* @author dingchao
* @date 2025/5/29
* @modified by:
*/
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "real-name")
public class RealNameVerifyConfig {
/**
* 数据宝api序列号
*/
private String apiKey;
/**
* 数据宝客户端加密私钥
*/
private String clientPrivateKey;
/**
* 数据宝服务端加密公钥
*/
private String serverPublicKey;
}

View File

@ -1,10 +1,8 @@
package com.cdzy.orders.config; package com.cdzy.orders.config;
import com.cdzy.orders.uitls.WechatUtil;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**

View File

@ -3,12 +3,13 @@ package com.cdzy.orders.controller;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cdzy.common.model.JsonResult; import com.cdzy.common.model.JsonResult;
import com.cdzy.orders.model.dto.req.ReqEbikeUserDto; import com.cdzy.orders.model.dto.req.ReqEbikeUserDto;
import com.cdzy.orders.model.dto.req.ReqUserValidateDto;
import com.cdzy.orders.model.dto.req.ReqWechatInfoDto; import com.cdzy.orders.model.dto.req.ReqWechatInfoDto;
import com.cdzy.orders.model.dto.res.EbikeUserDto; import com.cdzy.orders.model.dto.res.EbikeUserDto;
import com.cdzy.orders.model.entity.EbikeUser; import com.cdzy.orders.model.entity.EbikeUser;
import com.cdzy.orders.model.entity.EbikeUserOrders; import com.cdzy.orders.model.entity.EbikeUserOrders;
import com.cdzy.orders.service.UserOrdersService; import com.cdzy.orders.service.UserOrdersService;
import com.cdzy.orders.uitls.WechatUtil; import com.cdzy.orders.uitls.VerifyUtil;
import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.paginate.Page;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -30,7 +31,7 @@ public class EbikeUserController {
@Autowired @Autowired
private UserOrdersService userOrdersService; private UserOrdersService userOrdersService;
@Resource @Resource
private WechatUtil wechatUtil; private VerifyUtil wechatUtil;
/** /**
* 用户微信无感登录 * 用户微信无感登录
@ -73,6 +74,17 @@ public class EbikeUserController {
return JsonResult.success("解密成功", jsonObject.getString("phoneNumber")); return JsonResult.success("解密成功", jsonObject.getString("phoneNumber"));
} }
/**
* 验证用户实名
*
* @param reqUserValidateDto 验证用户实名请求
* @return 验证结果
*/
@PostMapping("/verifyRealName")
public JsonResult<?> verifyRealName(@RequestBody ReqUserValidateDto reqUserValidateDto) {
return wechatUtil.verifyRealName(reqUserValidateDto);
}
/** /**
* 添加用户信息 * 添加用户信息

View File

@ -0,0 +1,33 @@
package com.cdzy.orders.model.dto.req;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 用户实名验证dto
*
* @author dingchao
* @date 2025/5/27
* @modified by:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReqUserValidateDto implements Serializable {
/**
* 用户id
*/
private String userId;
/**
* 姓名
*/
private String name;
/**
* 身份证号
*/
private String idCard;
}

View File

@ -0,0 +1,218 @@
package com.cdzy.orders.uitls;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.cdp.product.security.context.SecurityContext;
import com.cdp.product.security.exception.DecryptFailureException;
import com.cdp.product.security.exception.EncryptFailureException;
import com.cdp.product.security.exception.SignFailureException;
import com.cdp.product.security.strategyImpl.RSAStrategyImpl;
import com.cdzy.common.model.JsonResult;
import com.cdzy.orders.config.RealNameVerifyConfig;
import com.cdzy.orders.config.WechatConfig;
import com.cdzy.orders.model.dto.req.ReqUserValidateDto;
import com.cdzy.orders.model.entity.EbikeUser;
import com.cdzy.orders.service.EbikeUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
/**
* 验证工具类
*
* @author dingchao
* @date 2025/4/9
* @modified by:
*/
@Slf4j
@Service
public class VerifyUtil {
/**
* 微信登录API地址
*/
private static final String WECHAT_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";
/**
* 数据宝实名认证API地址
*/
private static final String REALNAME_VERIFY_URL = "https://api.chinadatapay.com/communication/personal/1882";
/**
* OkHttpClient实例
*/
private static OkHttpClient client = null;
@Resource
private WechatConfig wechatConfig;
@Resource
private RealNameVerifyConfig realNameVerifyConfig;
@Resource
private EbikeUserService ebikeUserService;
/**
* 微信工具类构造函数
*
*/
public VerifyUtil() throws IOException {
client = new OkHttpClient();
}
/**
* 获取openId
*
* @param code 微信登录返回的code
* @return openId
*/
public JSONObject wechatAuthority(String code){
Map<String, String> params = new HashMap<>();
params.put("appid", wechatConfig.getAppId());
params.put("secret", wechatConfig.getAppSecret());
params.put("js_code", code);
params.put("grant_type", "authorization_code");
return httpGet("获取openId", WECHAT_LOGIN_URL, params);
}
/**
* 解密微信用户信息
*
* @param encryptedData 加密数据
* @param sessionKey 会话密钥
* @param iv 初始向量
* @return 解密后的用户信息
*/
public String decryptData(String encryptedData, String sessionKey, String iv) {
try {
// 添加 BouncyCastle 安全提供器
Security.addProvider(new BouncyCastleProvider());
// Base64 解码参数
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey);
byte[] ivBytes = Base64.getDecoder().decode(iv);
// 初始化 AES-CBC 解密器
SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 执行解密
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("解密失败", e);
return null;
}
}
/**
* 验证用户实名
*
* @param reqUserValidateDto 验证用户实名请求
* @return 验证结果
*/
public JsonResult<?> verifyRealName(ReqUserValidateDto reqUserValidateDto) {
EbikeUser ebikeUser = ebikeUserService.getById(reqUserValidateDto.getUserId());
if (ebikeUser == null) {
log.error("用户不存在");
return JsonResult.failed("用户不存在");
}
try {
SecurityContext securityContext = new SecurityContext();
//设置加解密上下类调RSA加解密类
securityContext.setSecurityImpl(new RSAStrategyImpl());
//设置加解密上下类调RSA加解密类加签
securityContext.setSign(new RSAStrategyImpl());
// 1. 加密数据
Map<String, String> params = new HashMap<>();
params.put("name", securityContext.encrypt(reqUserValidateDto.getName(), realNameVerifyConfig.getServerPublicKey()));
params.put("idcard", securityContext.encrypt(reqUserValidateDto.getIdCard(), realNameVerifyConfig.getServerPublicKey()));
params.put("timestamp", securityContext.encrypt(String.valueOf(System.currentTimeMillis()), realNameVerifyConfig.getServerPublicKey()));
String sign = securityContext.sign(params, realNameVerifyConfig.getClientPrivateKey());
params.put("sign", sign);
params.put("key", realNameVerifyConfig.getApiKey());
// 2. 验证用户实名
JSONObject result = httpGet("验证用户实名", REALNAME_VERIFY_URL, params);
if (result == null) {
log.error("验证用户实名失败");
return JsonResult.failed("验证用户实名失败");
}
if ("10000".equals(result.getString("code"))){
// 3. 解密结果
String data = securityContext.decrypt(result.getString("data"), realNameVerifyConfig.getClientPrivateKey());
JSONObject jsonData = JSON.parseObject(data);
if("10000".equals(jsonData.getString("code"))&&"1".equals(jsonData.getJSONObject("data").getString("state"))){
// 4. 更新用户实名信息
return JsonResult.success("验证用户实名成功");
}
}else{
log.error("验证用户实名失败,{} {}", result.getString("code"), result.getString("message"));
return JsonResult.failed(result.getString("code") +", "+ result.getString("message"));
}
}catch (EncryptFailureException e) {
log.error("加密失败", e);
return JsonResult.failed("加密失败");
}catch (SignFailureException e) {
log.error("签名失败", e);
return JsonResult.failed("签名失败");
} catch (DecryptFailureException e) {
log.error("结果解密失败", e);
return JsonResult.failed("结果解密失败");
}
}
/**
* 发送HTTP GET请求
*
* @param func_ 功能描述
* @param url 请求URL
* @param params 请求参数
* @return 响应结果
*/
private JSONObject httpGet(String func_, String url, Map<String, String> params) {
StringJoiner paramJoiner = new StringJoiner("&");
for (Map.Entry<String, String> entry : params.entrySet()) {
paramJoiner.add(entry.getKey() + "=" + entry.getValue());
}
String requestUrl = url + "?" + paramJoiner.toString();
Request request = new Request.Builder()
.url(requestUrl)
.method("GET", null)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
if (response.body() != null) {
return JSON.parseObject(response.body().string());
}
log.error("{}, 请求{}无返回结果", func_, url);
return null;
}
if (response.body() != null) {
log.error("{}, 请求{}失败, {}", func_, url, response.body().string());
}else {
log.error("{}, 请求{}失败", func_, url);
}
return null;
} catch (Exception e) {
log.error("{}, 请求{}失败", func_, url, e);
return null;
}
}
}

View File

@ -1,112 +0,0 @@
package com.cdzy.orders.uitls;
import com.alibaba.fastjson2.JSONObject;
import com.cdzy.orders.config.WechatConfig;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
/**
* 微信工具类
*
* @author dingchao
* @date 2025/4/9
* @modified by:
*/
@Slf4j
@Service
public class WechatUtil {
/**
* 微信登录API地址
*/
private static final String WECHAT_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";
/**
* OkHttpClient实例
*/
private static OkHttpClient client = null;
@Resource
private WechatConfig wechatConfig;
/**
* 微信工具类构造函数
*
*/
public WechatUtil() throws IOException {
client = new OkHttpClient();
}
/**
* 获取openId
*
* @param code 微信登录返回的code
* @return openId
*/
public JSONObject wechatAuthority(String code){
Request request = new Request.Builder()
.url(WECHAT_LOGIN_URL + "?appid=" + wechatConfig.getAppId() + "&secret=" + wechatConfig.getAppSecret() + "&js_code=" + code + "&grant_type=authorization_code")
.build();
try(Response response = client.newCall(request).execute()) {
if(response.isSuccessful()) {
if (response.body()!= null) {
String result = response.body().string();
JSONObject jsonObject = JSONObject.parseObject(result);
log.info("result: {}", jsonObject);
return jsonObject;
}
log.error("获取openId失败");
return null;
}
log.error("获取openId失败");
return null;
}catch (IOException e){
log.error("获取openId失败", e);
return null;
}
}
/**
* 解密微信用户信息
*
* @param encryptedData 加密数据
* @param sessionKey 会话密钥
* @param iv 初始向量
* @return 解密后的用户信息
*/
public String decryptData(String encryptedData, String sessionKey, String iv) {
try {
// 添加 BouncyCastle 安全提供器
Security.addProvider(new BouncyCastleProvider());
// Base64 解码参数
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey);
byte[] ivBytes = Base64.getDecoder().decode(iv);
// 初始化 AES-CBC 解密器
SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 执行解密
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}

View File

@ -7,7 +7,7 @@ spring:
name: ebike-orders name: ebike-orders
cloud: cloud:
nacos: nacos:
server-addr: 192.168.2.226:8848 # nacos server-addr: 127.0.0.1:8848 # nacos
username: nacos username: nacos
password: nacos password: nacos
jackson: jackson:
@ -92,7 +92,10 @@ minio:
access-key: eQtGmQBEsGxNHrTd7AkJ # 访问密钥 access-key: eQtGmQBEsGxNHrTd7AkJ # 访问密钥
secret-key: Zg6X6j0kgUT1fGsGSgoCZWu6fgL8F3Kw1FfoX4yJ # 私有密钥 secret-key: Zg6X6j0kgUT1fGsGSgoCZWu6fgL8F3Kw1FfoX4yJ # 私有密钥
bucket-name: test bucket-name: test
real-name:
api-key: 123456789
client-private-pey: 123456789
server-public-key: 123456789
management: management:
endpoints: endpoints:
web: web:

View File

@ -202,7 +202,7 @@ public class EbikeWxPaymentController {
/** /**
* 退款申请用户交易记录 * 退款申请用户交易记录
* *
* @param reqTradeRecordDto * @param reqTradeRecordDto 查询条件
* @return * @return
*/ */
@PostMapping("/refundApplyTradeRecord") @PostMapping("/refundApplyTradeRecord")
@ -214,7 +214,7 @@ public class EbikeWxPaymentController {
/** /**
* 退款申请用户退款记录 * 退款申请用户退款记录
* *
* @param reqRefundRecordDto * @param reqRefundRecordDto 查询条件
* @return * @return
*/ */
@PostMapping("/refundApplyRefundRecord") @PostMapping("/refundApplyRefundRecord")

View File

@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletRequest;
* @modified by: * @modified by:
*/ */
public interface WxPayService { public interface WxPayService {
/** /**
* 关闭订单 * 关闭订单
* *

View File

@ -17,6 +17,7 @@ import com.cdzy.payment.model.enums.RefoundApplySource;
import com.cdzy.payment.model.enums.RefundProcessState; import com.cdzy.payment.model.enums.RefundProcessState;
import com.cdzy.payment.service.EbikePaymentService; import com.cdzy.payment.service.EbikePaymentService;
import com.cdzy.payment.service.EbikeRefundService; import com.cdzy.payment.service.EbikeRefundService;
import com.cdzy.payment.service.EbikeUserService;
import com.cdzy.payment.service.WxPayService; import com.cdzy.payment.service.WxPayService;
import com.cdzy.payment.utils.HttpServletUtils; import com.cdzy.payment.utils.HttpServletUtils;
import com.cdzy.payment.utils.MapUtils; import com.cdzy.payment.utils.MapUtils;
@ -85,6 +86,8 @@ public class WxPayServiceImpl implements WxPayService {
@Resource @Resource
private EbikeRefundService ebikeRefundService; private EbikeRefundService ebikeRefundService;
@Resource @Resource
private EbikeUserService ebikeUserService;
@Resource
private OrdersFeignClient ordersFeignClient; private OrdersFeignClient ordersFeignClient;
@Resource @Resource
private MaintenanceFeignClient maintenanceFeignClient; private MaintenanceFeignClient maintenanceFeignClient;

View File

@ -66,9 +66,6 @@ task-scheduler-pool:
threadNamePrefix: task-scheduled- threadNamePrefix: task-scheduled-
waitForTasksToCompleteOnShutdown: true waitForTasksToCompleteOnShutdown: true
awaitTerminationSeconds: 30 awaitTerminationSeconds: 30
geo-coding:
api-url: https://apis.map.qq.com/ws/geocoder/v1
access-key: BECBZ-EJIEQ-LUU5N-B5ISQ-3TLMZ-BXFLG
management: management:
endpoints: endpoints: