拉卡拉 入网电子合同申请 相关业务逻辑,修改加密方法

This commit is contained in:
Jack 2025-05-15 15:53:29 +08:00
parent 40beddaaa5
commit 8eb9cf3ce6
16 changed files with 439 additions and 156 deletions

View File

@ -41,6 +41,7 @@ public class LklLedgerEc implements Serializable {
private String ec_status;
private Long ec_apply_id;
private String result_url;
private String notify_url;
private Integer status;
private Date created_at;
private Date updated_at;

View File

@ -14,6 +14,9 @@ import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 结巴分词工具类
*/
@Component
public class JiebaUtils {

View File

@ -0,0 +1,159 @@
/*
* Copyright (c) 2025. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
* Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
* Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
* Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
* Vestibulum commodo. Ut rhoncus gravida arcu.
*/
package com.suisung.mall.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);
/**
* 使用私钥对数据进行 SHA256withRSA 签名
*
* @param data 待签名的数据
* @param privateKey 私钥(Base64编码)
* @return 签名结果(Base64编码)
*/
public static String signSHA256withRSA(String data, String privateKey) {
try {
// 解码私钥
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
// 生成私钥对象
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey priKey = keyFactory.generatePrivate(keySpec);
// 初始化签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(priKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
// 生成签名并编码为Base64
byte[] signBytes = signature.sign();
return Base64.getEncoder().encodeToString(signBytes);
} catch (NoSuchAlgorithmException e) {
logger.error("不支持的加密算法: {}", e.getMessage(), e);
} catch (InvalidKeySpecException e) {
logger.error("私钥格式错误: {}", e.getMessage(), e);
} catch (InvalidKeyException e) {
logger.error("无效的私钥: {}", e.getMessage(), e);
} catch (SignatureException e) {
logger.error("签名处理异常: {}", e.getMessage(), e);
} catch (Exception e) {
logger.error("签名过程发生未知异常: {}", e.getMessage(), e);
}
return null;
}
/**
* 使用公钥验证 SHA256withRSA 签名
*
* @param data 原始数据
* @param sign 签名结果(Base64编码)
* @param publicKey 公钥(Base64编码)
* @return 验证结果
*/
public static boolean verifySHA256withRSA(String data, String sign, String publicKey) {
try {
// 解码公钥
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
// 生成公钥对象
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
// 初始化签名验证
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(pubKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
// 验证签名
byte[] signBytes = Base64.getDecoder().decode(sign);
return signature.verify(signBytes);
} catch (NoSuchAlgorithmException e) {
logger.error("不支持的加密算法: {}", e.getMessage(), e);
} catch (InvalidKeySpecException e) {
logger.error("公钥格式错误: {}", e.getMessage(), e);
} catch (InvalidKeyException e) {
logger.error("无效的公钥: {}", e.getMessage(), e);
} catch (SignatureException e) {
logger.error("签名验证异常: {}", e.getMessage(), e);
} catch (Exception e) {
logger.error("验证过程发生未知异常: {}", e.getMessage(), e);
}
return false;
}
/**
* 生成 RSA 密钥对
*
* @param keySize 密钥长度推荐 2048
* @return 密钥对
*/
public static KeyPair generateKeyPair(int keySize) {
KeyPairGenerator keyPairGen = null;
try {
keyPairGen = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
keyPairGen.initialize(keySize);
return keyPairGen.generateKeyPair();
}
/**
* 获取公钥的 Base64 编码字符串
*/
public static String getPublicKeyString(KeyPair keyPair) {
PublicKey publicKey = keyPair.getPublic();
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}
/**
* 获取私钥的 Base64 编码字符串
*/
public static String getPrivateKeyString(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
return Base64.getEncoder().encodeToString(privateKey.getEncoded());
}
// 示例用法
public static void main(String[] args) {
try {
// 生成密钥对
KeyPair keyPair = generateKeyPair(2048);
String publicKey = getPublicKeyString(keyPair);
String privateKey = getPrivateKeyString(keyPair);
// 待签名数据
String data = "Hello, RSA Signature!";
// 签名
String signature = signSHA256withRSA(data, privateKey);
System.out.println("签名结果: " + signature);
// 验证
boolean isValid = verifySHA256withRSA(data, signature, publicKey);
System.out.println("验证结果: " + isValid);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -11,7 +11,6 @@ package com.suisung.mall.pay.utils;
import cn.hutool.core.util.StrUtil;
import com.lkl.laop.sdk.Config2;
import com.lkl.laop.sdk.LKLSDK;
import com.lkl.laop.sdk.auth.PrivateKeySigner;
import com.suisung.mall.common.exception.ApiException;
import com.suisung.mall.common.utils.I18nUtil;
import lombok.extern.slf4j.Slf4j;
@ -76,6 +75,7 @@ public class LakalaUtil {
}
}
/**
* 获取 body 请求数据
*
@ -130,7 +130,6 @@ public class LakalaUtil {
}
}
/**
* 签名验证
*
@ -155,9 +154,9 @@ public class LakalaUtil {
}
}
/**
* 签名校验
*
* @param authorization
* @param reqBody
* @param lklNotifyCerPath
@ -176,11 +175,9 @@ public class LakalaUtil {
return false;
}
StringBuilder preSignData = new StringBuilder();
preSignData.append(timestamp).append("\n")
.append(nonceStr).append("\n")
.append(reqBody).append("\n");
String preSignDataStr = preSignData.toString();
String preSignDataStr = timestamp + "\n" +
nonceStr + "\n" +
reqBody + "\n";
log.debug("拉卡拉签名明文内容:{}", preSignDataStr);
if (verify(lklNotifyCer, preSignDataStr.getBytes(StandardCharsets.UTF_8), signature)) {
log.debug("验签成功");
@ -237,4 +234,5 @@ public class LakalaUtil {
return map;
}
}

View File

@ -211,9 +211,14 @@ public class EsignContractServiceImpl extends BaseServiceImpl<EsignContractMappe
//发起接口请求
EsignHttpResponse createByDocTemplate = EsignHttpHelper.doCommHttp(serverUrl, apiAddr, requestType, jsonParams, header, debug);
if (createByDocTemplate.getStatus() != 200 || createByDocTemplate.getBody() == null) {
log.error("e签宝请求失败返回状态码{}", createByDocTemplate.getStatus());
return Pair.of(false, "e签宝请求失败状态码" + createByDocTemplate.getStatus());
log.debug("发起合同签署流程返回消息:{},{}", createByDocTemplate.getStatus(), createByDocTemplate.getBody());
if (createByDocTemplate.getStatus() != 200) {
if (createByDocTemplate.getBody() != null) {
JSONObject resBody = JSONUtil.parseObj(createByDocTemplate.getBody());
log.error("e签宝请求失败返回状态码{}, {}", createByDocTemplate.getStatus(), resBody.getStr("message"));
return Pair.of(false, "e签宝请求失败{}" + resBody.getStr("message"));
}
return Pair.of(false, "e签宝请求失败!");
}
JSONObject jsonObject = JSONUtil.parseObj(createByDocTemplate.getBody());

View File

@ -30,6 +30,12 @@ public class LakalaController extends BaseControllerImpl {
@Resource
private LakalaApiService lakalaPayService;
@ApiOperation(value = "测试案例", notes = "测试案例")
@RequestMapping(value = "/testcase", method = RequestMethod.POST)
public Object testcase(@RequestBody JSONObject paramsJSON) {
return lakalaPayService.applyLedgerMerEc(paramsJSON.getStr("mchMobile"));
}
@ApiOperation(value = "本地文件转base64", notes = "本地文件转base64")
@RequestMapping(value = "/file2base64", method = RequestMethod.POST)
public String file2Base64(@RequestParam("file") MultipartFile file) throws IOException {

View File

@ -15,6 +15,7 @@ import com.suisung.mall.common.modules.lakala.LklBanks;
import com.suisung.mall.common.service.impl.BaseControllerImpl;
import com.suisung.mall.shop.lakala.service.LklBanksService;
import com.suisung.mall.shop.lakala.service.impl.LklTkServiceImpl;
import com.suisung.mall.shop.lakala.utils.LakalaUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.data.util.Pair;
@ -42,7 +43,7 @@ public class LklTkController extends BaseControllerImpl {
public String decryptLklTkData(@RequestParam(name = "data") String data,
@RequestParam(name = "key") String key) {
return lklTkService.decryptNotifyData(key, data);
return LakalaUtil.decryptNotifyData(key, data);
}

View File

@ -23,20 +23,16 @@ import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.constant.CommonConstant;
import com.suisung.mall.common.exception.ApiException;
import com.suisung.mall.common.feignService.ShopService;
import com.suisung.mall.common.modules.lakala.LklLedgerEc;
import com.suisung.mall.common.modules.lakala.LklLedgerMember;
import com.suisung.mall.common.modules.lakala.LklLedgerMerReceiverBind;
import com.suisung.mall.common.modules.lakala.LklLedgerReceiver;
import com.suisung.mall.common.modules.store.ShopMchEntry;
import com.suisung.mall.common.modules.store.ShopStoreBase;
import com.suisung.mall.common.utils.I18nUtil;
import com.suisung.mall.common.utils.StringUtils;
import com.suisung.mall.common.utils.UploadUtil;
import com.suisung.mall.common.utils.*;
import com.suisung.mall.shop.esign.service.EsignContractFillingFileService;
import com.suisung.mall.shop.esign.service.EsignContractService;
import com.suisung.mall.shop.lakala.service.LakalaApiService;
import com.suisung.mall.shop.lakala.service.LklLedgerMemberService;
import com.suisung.mall.shop.lakala.service.LklLedgerMerReceiverBindService;
import com.suisung.mall.shop.lakala.service.LklLedgerReceiverService;
import com.suisung.mall.shop.lakala.service.*;
import com.suisung.mall.shop.lakala.utils.LakalaUtil;
import com.suisung.mall.shop.store.service.ShopMchEntryService;
import com.suisung.mall.shop.store.service.ShopStoreBaseService;
@ -45,6 +41,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@ -53,6 +51,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@ -119,6 +118,10 @@ public class LakalaApiServiceImpl implements LakalaApiService {
@Resource
private ShopStoreBaseService shopStoreBaseService;
@Lazy
@Autowired
private LklLedgerEcService lklLedgerEcService;
/**
* 初始化 拉卡拉SDK
*
@ -493,6 +496,7 @@ public class LakalaApiServiceImpl implements LakalaApiService {
reqData.put("acct_type_code", shopMchEntry.getAccount_type());//57 对公 58 对私
reqData.put("acct_no", shopMchEntry.getAccount_number());
reqData.put("acct_name", shopMchEntry.getAccount_holder_name());
reqData.put("remark", "申请入网电子合同");
String domain = projectDomain;
if (isProd()) {
@ -538,16 +542,66 @@ public class LakalaApiServiceImpl implements LakalaApiService {
ecParams.put("D11", signDate);
ecParams.put("D12", signDate);
ecParams.put("E1", "桂平发发网络有限公司");
ecParams.put("E2", "平台商户入驻合同协议");
ecParams.put("E2", "商户入驻小发同城平台合同协议");
ecParams.put("E3", "0");
ecParams.put("E4", "70");
ecParams.put("E5", "桂平发发网络有限公司");
ecParams.put("E7", signDate);
ecParams.put("E8", shopMchEntry.getAccount_holder_name());
reqData.put("ec_content_parameters", ecParams);
return null;
// 该字段是字符串不是json
reqData.put("ec_content_parameters", ecParams.toString());
JSONObject reqBody = new JSONObject();
reqBody.put("req_time", DateTimeUtils.formatDateTime(LocalDateTime.now(), "yyyyMMddHHmmss"));
reqBody.put("version", "3.0");
reqBody.put("req_data", reqData);
String reqUrl = serverUrl + "/sit/api/v3/mms/open_api/ec/apply";
if (isProd()) {
reqUrl = serverUrl + "/api/v3/mms/open_api/ec/apply";
}
String privateKey = LakalaUtil.getResourceFile(priKeyPath, false, true);
String authorization = LakalaUtil.genAuthorization(privateKey, appId, serialNo, reqBody.toString());
JSONObject header = new JSONObject();
header.put("Authorization", authorization);
ResponseEntity<JSONObject> response = RestTemplateHttpUtil.sendPostBodyBackEntity(reqUrl, header, reqBody, JSONObject.class);
if (ObjectUtil.isEmpty(response) || response.getStatusCode() != HttpStatus.OK) {
return Pair.of(false, "商家入网申请电子合同:请求失败");
}
JSONObject respBody = response.getBody();
if (ObjectUtil.isNotEmpty(respBody) && !lklSuccessCode.equals(respBody.getStr("code"))) {
String errMsg = StrUtil.isBlank(respBody.getStr("msg")) ? "返回状态有误" : respBody.getStr("msg");
return Pair.of(false, "商家入网申请电子合同失败:" + errMsg);
}
JSONObject respData = respBody.getJSONObject("resp_data");
if (respBody.getJSONObject("resp_data") == null) {
return Pair.of(false, "商家入网申请电子合同失败:返回数据有误");
}
// 商家入网申请电子合同处理数据
// 先写入本地数据库表中
LklLedgerEc record = new LklLedgerEc();
record.setMch_id(shopMchEntry.getId());
record.setMch_mobile(shopMchEntry.getLogin_mobile());
record.setReq_params(reqBody.toString());
record.setNotify_url(retUrl);
record.setEc_apply_id(respData.getLong("ec_apply_id"));
record.setResult_url(respData.getStr("result_url"));
record.setResp_body(respBody.toString());
Boolean success = lklLedgerEcService.saveOrUpdateByMchId(record);
if (!success) {
return Pair.of(false, "商家入网申请电子合同失败:本地数据保存失败");
}
return Pair.of(true, "商家入网申请电子合同成功");
}

View File

@ -24,6 +24,7 @@ import com.suisung.mall.shop.lakala.mapper.LklLedgerReceiverMapper;
import com.suisung.mall.shop.lakala.service.LakalaApiService;
import com.suisung.mall.shop.lakala.service.LklLedgerMemberService;
import com.suisung.mall.shop.lakala.service.LklLedgerReceiverService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -31,6 +32,7 @@ import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class LklLedgerReceiverServiceImpl extends BaseServiceImpl<LklLedgerReceiverMapper, LklLedgerReceiver> implements LklLedgerReceiverService {
@ -207,13 +209,16 @@ public class LklLedgerReceiverServiceImpl extends BaseServiceImpl<LklLedgerRecei
public Boolean innerApplyLedgerReceiver(String merCupNo, Long platformId) {
JSONArray buildApplyLedgerReceiverReqParams = buildApplyLedgerReceiverReqParams(platformId);
if (CollectionUtil.isEmpty(buildApplyLedgerReceiverReqParams)) {
log.error("没有平台信息或代理商信息");
return false;
}
int success = 0;
for (JSONObject reqParam : buildApplyLedgerReceiverReqParams.jsonIter()) {
log.debug("申请分账接收方参数:{}", reqParam.toString());
CommonResult result = lakalaApiService.applyLedgerReceiver(reqParam);
if (result == null || result.getCode() != 200) {
log.error("申请分账接收方出错:{}", result.getMsg());
continue;
}

View File

@ -41,18 +41,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@ -104,108 +93,6 @@ public class LklTkServiceImpl {
@Autowired
private RedisService redisService;
/**
* 拉卡拉使用私钥加密数据
*/
public static String encryptNotifyData(String priKey, String data) {
ByteArrayOutputStream out = null;
try {
logger.debug("拉卡拉tk加密私钥{}", priKey);
logger.debug("拉卡拉tk待加密data数据{}", data);
// 解码私钥
byte[] keyBytes = Base64.getDecoder().decode(priKey);
// 生成私钥对象
PrivateKey privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
// 初始化加密器
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 分段加密
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
out = new ByteArrayOutputStream();
for (int offset = 0; offset < dataBytes.length; offset += 117) {
int blockSize = Math.min(117, dataBytes.length - offset);
byte[] encryptedBlock = cipher.doFinal(dataBytes, offset, blockSize);
out.write(encryptedBlock);
}
// 编码为Base64字符串
String encryptDataStr = Base64.getEncoder().encodeToString(out.toByteArray());
logger.debug("拉卡拉tk数据加密结果{}", encryptDataStr);
return encryptDataStr;
} catch (Exception e) {
throw new RuntimeException("私钥加密失败", e);
} finally {
closeQuietly(out);
}
}
/**
* 安全关闭流
*/
private static void closeQuietly(ByteArrayOutputStream out) {
if (out != null) {
try {
out.close();
} catch (Exception e) {
// 忽略异常
}
}
}
/**
* 拉卡拉异步通知数据公钥解密
*
* @param pubKey Base64公钥
* @param data Base64数据
* @return 解密字符串
*/
public String decryptNotifyData(String pubKey, String data) {
ByteArrayOutputStream out = null;
try {
logger.debug("拉卡拉tk解密公钥{}", pubKey);
logger.debug("拉卡拉tk待解密data数据{}", data);
Base64.Decoder decoder = Base64.getDecoder();
byte[] keyBytes = decoder.decode(pubKey.getBytes(StandardCharsets.UTF_8));
byte[] dataBytes = decoder.decode(data.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("RSA");
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(keyBytes));
cipher.init(Cipher.DECRYPT_MODE, publicKey);
out = new ByteArrayOutputStream();
byte[] cache;
for (int i = 0, offset = 0, length = dataBytes.length; length - offset > 0; i++, offset = i * 128) {
if (length - offset > 128) {
cache = cipher.doFinal(dataBytes, offset, 128);
} else {
cache = cipher.doFinal(dataBytes, offset, length - offset);
}
out.write(cache, 0, cache.length);
}
String decodedDataStr = out.toString("UTF-8");
logger.debug("拉卡拉tk数据解密结果{}", decodedDataStr);
return decodedDataStr;
} catch (IllegalArgumentException e) {
logger.error("Base64解码失败: {}", e.getMessage(), e);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
logger.error("RSA算法初始化失败: {}", e.getMessage(), e);
} catch (InvalidKeySpecException | InvalidKeyException e) {
logger.error("私钥格式或类型错误: {}", e.getMessage(), e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
logger.error("解密数据块大小或填充错误: {}", e.getMessage(), e);
} catch (Exception e) {
logger.error("解密过程中出现未知异常: {}", e.getMessage(), e);
} finally {
closeQuietly(out);
}
return null;
}
protected String buildLklTkUrl(String urlPath) {
return tkServerUrl + urlPath;
@ -610,8 +497,8 @@ public class LklTkServiceImpl {
}
// 公钥解密出来的数据
String notifyPubKey = LakalaUtil.getResourceFileWithEndSeq(notifyPubKeyPath, false);
String data = decryptNotifyData(notifyPubKey, srcData);
String notifyPubKey = LakalaUtil.getResourceFile(notifyPubKeyPath, false, false);
String data = LakalaUtil.decryptNotifyData(notifyPubKey, srcData);
if (StrUtil.isBlank(data)) {
return new JSONObject().set("code", "500").set("message", "密文解密出错!");
}

View File

@ -13,23 +13,28 @@ import com.lkl.laop.sdk.Config2;
import com.lkl.laop.sdk.LKLSDK;
import com.suisung.mall.common.exception.ApiException;
import com.suisung.mall.common.utils.I18nUtil;
import com.suisung.mall.common.utils.RSAUtil;
import com.suisung.mall.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.core.io.ClassPathResource;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class LakalaUtil {
@ -58,10 +63,17 @@ public class LakalaUtil {
* @return
*/
public static String getResourceFile(String fileName) {
return getResourceFileWithEndSeq(fileName, true);
return getResourceFile(fileName, true, false);
}
public static String getResourceFileWithEndSeq(String fileName, boolean keepLineBreaks) {
/**
* 获取配置文件内容
*
* @param fileName recource 文件夹下的路径palyKey/wx/lakala_public_key.cer
* @param keepLineBreaks 保留换行符
* @return
*/
public static String getResourceFile(String fileName, boolean keepLineBreaks, boolean stripPemHeaders) {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = new ClassPathResource(fileName).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
@ -71,6 +83,9 @@ public class LakalaUtil {
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append(endSeq);
}
if (stripPemHeaders) {
return stripPemHeaders(stringBuilder.toString());
}
return stringBuilder.toString();
} catch (IOException e) {
// 记录异常信息
@ -79,6 +94,76 @@ public class LakalaUtil {
}
}
/**
* 拉卡拉使用私钥加密数据
*/
public static String encryptNotifyData(String priKey, String data) {
ByteArrayOutputStream out = null;
try {
log.debug("拉卡拉tk加密私钥{}", priKey);
log.debug("拉卡拉tk待加密data数据{}", data);
// 解码私钥
byte[] keyBytes = java.util.Base64.getDecoder().decode(priKey);
// 生成私钥对象
PrivateKey privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
// 初始化加密器
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 分段加密
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
out = new ByteArrayOutputStream();
for (int offset = 0; offset < dataBytes.length; offset += 117) {
int blockSize = Math.min(117, dataBytes.length - offset);
byte[] encryptedBlock = cipher.doFinal(dataBytes, offset, blockSize);
out.write(encryptedBlock);
}
// 编码为Base64字符串
String encryptDataStr = java.util.Base64.getEncoder().encodeToString(out.toByteArray());
log.debug("拉卡拉tk数据加密结果{}", encryptDataStr);
return encryptDataStr;
} catch (Exception e) {
throw new RuntimeException("私钥加密失败", e);
} finally {
closeQuietly(out);
}
}
/**
* 安全关闭流
*/
private static void closeQuietly(ByteArrayOutputStream out) {
if (out != null) {
try {
out.close();
} catch (Exception e) {
// 忽略异常
}
}
}
public static String stripPemHeaders(String key) {
if (key == null || key.isEmpty()) {
return key;
}
// 定义正则表达式匹配标签行任意数量连字符和空白字符
// 1. 包含BEGIN或END的标签行无论连字符数量
// 2. 单独的连字符不在标签行中的
// 3. 空白字符(空格制表符换行等)
Pattern pattern = Pattern.compile(
"-*BEGIN[^-]*-*|-*END[^-]*-*|-|\\s");
// 执行替换
Matcher matcher = pattern.matcher(key);
return matcher.replaceAll("");
}
/**
* 获取 body 请求数据
*
@ -133,7 +218,6 @@ public class LakalaUtil {
}
}
/**
* 签名验证
*
@ -158,7 +242,6 @@ public class LakalaUtil {
}
}
/**
* 签名校验
*
@ -239,4 +322,85 @@ public class LakalaUtil {
return map;
}
/**
* 拼接拉卡拉header中的Authorization字段
* 参考文档https://o.lakala.com/#/home/document/detail?id=33
*
* @param privateKey
* @param appId
* @param serialNo
* @param reqBody
* @return
*/
public static String genAuthorization(String privateKey, String appId, String serialNo, String reqBody) {
if (StrUtil.isBlank(privateKey) || StrUtil.isBlank(appId) || StrUtil.isBlank(serialNo) || StrUtil.isBlank(reqBody)) {
log.error("生产拉卡拉签名时缺少参数");
return "";
}
//待签字符串格式${appid}\n+${serialNo}\n+${timeStamp}\n+${nonceStr}\n+${body}\n
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = StringUtils.genRandomNumber(12);
String builder = appId + "\n" +
serialNo + "\n" +
timestamp + "\n" +
nonceStr + "\n" +
reqBody + "\n";
String signature = RSAUtil.signSHA256withRSA(builder, privateKey);
return String.format("LKLAPI-SHA256withRSA appid=\"%s\",serial_no=\"%s\",timestamp=\"%s\",nonce_str=\"%s\",signature=\"%s\"",
appId, serialNo, timestamp, nonceStr, signature);
}
/**
* 拉卡拉异步通知数据公钥解密
*
* @param pubKey Base64公钥
* @param data Base64数据
* @return 解密字符串
*/
public static String decryptNotifyData(String pubKey, String data) {
ByteArrayOutputStream out = null;
try {
log.debug("拉卡拉tk解密公钥{}", pubKey);
log.debug("拉卡拉tk待解密data数据{}", data);
java.util.Base64.Decoder decoder = java.util.Base64.getDecoder();
byte[] keyBytes = decoder.decode(pubKey.getBytes(StandardCharsets.UTF_8));
byte[] dataBytes = decoder.decode(data.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("RSA");
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(keyBytes));
cipher.init(Cipher.DECRYPT_MODE, publicKey);
out = new ByteArrayOutputStream();
byte[] cache;
for (int i = 0, offset = 0, length = dataBytes.length; length - offset > 0; i++, offset = i * 128) {
if (length - offset > 128) {
cache = cipher.doFinal(dataBytes, offset, 128);
} else {
cache = cipher.doFinal(dataBytes, offset, length - offset);
}
out.write(cache, 0, cache.length);
}
String decodedDataStr = out.toString("UTF-8");
log.debug("拉卡拉tk数据解密结果{}", decodedDataStr);
return decodedDataStr;
} catch (IllegalArgumentException e) {
log.error("Base64解码失败: {}", e.getMessage(), e);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
log.error("RSA算法初始化失败: {}", e.getMessage(), e);
} catch (InvalidKeySpecException | InvalidKeyException e) {
log.error("私钥格式或类型错误: {}", e.getMessage(), e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
log.error("解密数据块大小或填充错误: {}", e.getMessage(), e);
} catch (Exception e) {
log.error("解密过程中出现未知异常: {}", e.getMessage(), e);
} finally {
closeQuietly(out);
}
return null;
}
}

View File

@ -144,7 +144,7 @@ sf-express:
#拉卡拉进件配置
lakala:
#服务地址
server_url: https://test.wsmsd.cn/sit
server_url: https://test.wsmsd.cn
#应用Id
app_id: OP00000003
#商户证书序列号

View File

@ -144,7 +144,7 @@ sf-express:
#拉卡拉进件配置
lakala:
#服务地址
server_url: https://test.wsmsd.cn/sit
server_url: https://test.wsmsd.cn
#应用Id
app_id: OP00000003
#商户证书序列号
@ -164,7 +164,7 @@ lakala:
# 拉卡拉拓客进件配置
tk:
#服务地址
server_url: https://test.wsmsd.cn
server_url: https://test.wsmsd.cn/sit
client_id: lsycs
client_secret: XPa1HB5d55Ig0qV8
user_no: 29153396

View File

@ -171,7 +171,7 @@ lakala:
# #终端号码M0780629B2B收银台 M0780798专业化扫码
# term_no: M0780798
#服务地址
server_url: https://test.wsmsd.cn/sit
server_url: https://test.wsmsd.cn
#应用Id
app_id: OP00000003
#商户证书序列号

View File

@ -148,7 +148,7 @@ sf-express:
#拉卡拉进件配置
lakala:
#服务地址
server_url: https://test.wsmsd.cn/sit
server_url: https://test.wsmsd.cn
#应用Id
app_id: OP00000003
#商户证书序列号

View File

@ -148,7 +148,7 @@ sf-express:
#拉卡拉进件配置
lakala:
#服务地址
server_url: https://test.wsmsd.cn/sit
server_url: https://test.wsmsd.cn
#应用Id
app_id: OP00000003
#商户证书序列号