接口加密,分销
This commit is contained in:
1 parent
30176e7268
commit
c52bbc1a2c
15 files changed
+347
-19
No files matched your search
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eE
|
||||
Ud2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1y
|
||||
wsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/
|
||||
Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6o
|
||||
mstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZx
|
||||
AMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3J
|
||||
p09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6M
|
||||
kqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebt
|
||||
pV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtB
|
||||
UKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdP
|
||||
uHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgL
|
||||
DtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX
|
||||
6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJS
|
||||
OanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8o
|
||||
ZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+W
|
||||
ijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwv
|
||||
O+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD
|
||||
1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPY
|
||||
w5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0V
|
||||
FF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2
|
||||
SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMx
|
||||
yoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRsw
|
||||
HDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585
|
||||
E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD
|
||||
1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4
|
||||
pdry0NF+Wfjny8ATyFDKY08w
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w
|
||||
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJ
|
||||
a1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7Sd
|
||||
eQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Z
|
||||
r6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMp
|
||||
BWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVW
|
||||
ceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26k
|
||||
DwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSt5lLsmloOnhFHdh8NJa1L76U/yQWX+S2VPyyXgp7/9TNcbOlygQ0iD0+SXd828wrVkeHo67ELtcsLF/7SdeQ1mykvxEswKhAG7SCjaocq9SwMnSre7iHnrtnNKRRH8Sozds67sRJPePz8o8W/Zr6XKeEuStvZ3E6n2HD7Ug455qIyFMDJnY7ntb3AEWnGdhXsF9Nqi22yuqJrLQOMpBWu/pTFhpCPr2ECdMdulG491J+snR/UYJZu7Ro65O4Fb3d4EVC9EHKH2cQDLAkVWceFliYvihrutYvBkg9aygt5FaQZiUrfie0iVy5FvZLkO3nv+fm9lPTKNyadPb26kDwIDAQAB
|
||||
+9
-5
@@ -38,8 +38,8 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_C
|
||||
@Validated
|
||||
public class BrokerageCodeServiceImpl implements BrokerageCodeService {
|
||||
|
||||
/** v3:海报图印 trade_brokerage_code.code(不再用 member_user.user_code) */
|
||||
private static final String POSTER_VERSION_MARKER = "#poster-v3-code";
|
||||
/** v4:邀请链接改为 /invite.html?code=&userId=,二维码同步刷新 */
|
||||
private static final String POSTER_VERSION_MARKER = "#poster-v4-invite-html";
|
||||
|
||||
private static final String BROKERAGE_CODE_GENERATE_LOCK_KEY = "trade:brokerage-code:generate:";
|
||||
|
||||
@@ -152,8 +152,7 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
|
||||
if (ObjectUtil.isNotEmpty(user)) {
|
||||
userAvatar = user.getAvatar();
|
||||
}
|
||||
String posterUrl = ObjectUtil.isNotEmpty(currentCode.getUrl())
|
||||
? currentCode.getUrl() : generatePosterUrl(currentCode.getCode(), userId);
|
||||
String posterUrl = generatePosterUrl(currentCode.getCode(), userId);
|
||||
String posterPic = generatePosterPic(userAvatar, posterCode, posterUrl);
|
||||
if (ObjectUtil.isEmpty(posterPic)) {
|
||||
return currentCode;
|
||||
@@ -232,7 +231,12 @@ public class BrokerageCodeServiceImpl implements BrokerageCodeService {
|
||||
}
|
||||
|
||||
private String generatePosterUrl(String code, Long userId) {
|
||||
return brokerageConfiguration.getDownloadUrl() + "?code=" + code + "&userId=" + userId;
|
||||
String base = brokerageConfiguration.getDownloadUrl();
|
||||
if (base != null && base.endsWith("/")) {
|
||||
base = base.substring(0, base.length() - 1);
|
||||
}
|
||||
// 格式:http://download.htysapp.com/invite.html?code=569876&userId=90
|
||||
return base + "/invite.html?code=" + code + "&userId=" + userId;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-6
@@ -84,9 +84,9 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 获得一级推广人
|
||||
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
|
||||
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
|
||||
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
|
||||
if (firstUser == null) {
|
||||
return;
|
||||
}
|
||||
// 1.2 计算一级分佣
|
||||
@@ -98,7 +98,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
return;
|
||||
}
|
||||
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
|
||||
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
|
||||
if (secondUser == null) {
|
||||
return;
|
||||
}
|
||||
// 2.2 计算二级分佣
|
||||
@@ -124,9 +124,9 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 获得一级推广人
|
||||
// 1.1 获得一级推广人(人人分销:只要绑定了上级即可分佣,不校验上级推广员开关)
|
||||
BrokerageUserDO firstUser = brokerageUserService.getBindBrokerageUser(userId);
|
||||
if (firstUser == null || !BooleanUtil.isTrue(firstUser.getBrokerageEnabled())) {
|
||||
if (firstUser == null) {
|
||||
return;
|
||||
}
|
||||
// 1.1.1查看一级分销员是否已经分销过
|
||||
@@ -145,7 +145,7 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
return;
|
||||
}
|
||||
BrokerageUserDO secondUser = brokerageUserService.getBrokerageUser(firstUser.getBindUserId());
|
||||
if (secondUser == null || !BooleanUtil.isTrue(secondUser.getBrokerageEnabled())) {
|
||||
if (secondUser == null) {
|
||||
return;
|
||||
}
|
||||
// 2.1.1查看二级分销员是否已经分销过
|
||||
|
||||
+2
-7
@@ -189,14 +189,9 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
|
||||
}
|
||||
// 2.3 校验能否绑定
|
||||
validateCanBindUser(brokerageUser, bindUserId);
|
||||
// 2.3 绑定用户
|
||||
// 2.3 绑定用户(人人分销:新用户默认具备分销资格)
|
||||
if (isNewBrokerageUser) {
|
||||
Integer enabledCondition = tradeConfigService.getTradeConfig().getBrokerageEnabledCondition();
|
||||
if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
|
||||
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
|
||||
} else {
|
||||
brokerageUser.setBrokerageEnabled(false).setBrokerageTime(LocalDateTime.now());
|
||||
}
|
||||
brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
|
||||
brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
|
||||
} else {
|
||||
brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId)));
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.member.framework.crypto;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(RespBodyEncryptProperties.class)
|
||||
public class RespBodyEncryptConfiguration {
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.yudao.module.member.framework.crypto;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* App 敏感接口响应体加密配置
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "yudao.member.resp-encrypt")
|
||||
@Data
|
||||
public class RespBodyEncryptProperties {
|
||||
|
||||
/**
|
||||
* 是否开启(总开关)
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* RSA 私钥(PKCS#8 Base64,无 PEM 头尾)
|
||||
*/
|
||||
private String privateKeyBase64;
|
||||
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package cn.iocoder.yudao.module.member.framework.crypto;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对白名单接口的 CommonResult.data 做响应加密(需客户端 Header 声明)。
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "yudao.member.resp-encrypt", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class RespBodyEncryptResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final RespBodyEncryptProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private RespBodyEncryptSupport support;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (StrUtil.isBlank(properties.getPrivateKeyBase64())) {
|
||||
log.warn("[resp-encrypt] 未配置 yudao.member.resp-encrypt.private-key-base64,响应加密不可用");
|
||||
support = null;
|
||||
return;
|
||||
}
|
||||
support = new RespBodyEncryptSupport(properties.getPrivateKeyBase64(), objectMapper);
|
||||
log.info("[resp-encrypt] 响应加密已启用,白名单={}", RespBodyEncryptSupport.PATH_SUFFIXES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||
return support != null
|
||||
&& returnType.getMethod() != null
|
||||
&& returnType.getMethod().getReturnType() == CommonResult.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
if (!(body instanceof CommonResult) || support == null) {
|
||||
return body;
|
||||
}
|
||||
CommonResult<Object> result = (CommonResult<Object>) body;
|
||||
if (!result.isSuccess() || result.getData() == null) {
|
||||
return body;
|
||||
}
|
||||
|
||||
String path = request.getURI().getPath();
|
||||
if (!RespBodyEncryptSupport.matchPath(path)) {
|
||||
return body;
|
||||
}
|
||||
|
||||
List<String> encHeaders = request.getHeaders().get(RespBodyEncryptSupport.HEADER_RESP_ENC);
|
||||
String enc = (encHeaders != null && !encHeaders.isEmpty()) ? encHeaders.get(0) : null;
|
||||
if (!RespBodyEncryptSupport.wantEncrypt(enc)) {
|
||||
// 旧客户端未声明加密:仍返回明文,便于过渡
|
||||
return body;
|
||||
}
|
||||
|
||||
List<String> ekHeaders = request.getHeaders().get(RespBodyEncryptSupport.HEADER_RESP_EK);
|
||||
String ek = (ekHeaders != null && !ekHeaders.isEmpty()) ? ekHeaders.get(0) : null;
|
||||
try {
|
||||
result.setData(support.encryptData(result.getData(), ek));
|
||||
} catch (Exception e) {
|
||||
log.error("[resp-encrypt] 加密失败 path={} err={}", path, e.getMessage());
|
||||
return CommonResult.error(1_004_000_101, "响应加密失败,请升级客户端后重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package cn.iocoder.yudao.module.member.framework.crypto;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 响应体加密:客户端用 RSA 公钥加密随机 AES Key(Header),服务端 AES-GCM 加密 data。
|
||||
*/
|
||||
@Slf4j
|
||||
public class RespBodyEncryptSupport {
|
||||
|
||||
public static final String HEADER_RESP_ENC = "X-Resp-Enc";
|
||||
public static final String HEADER_RESP_EK = "X-Resp-Ek";
|
||||
|
||||
public static final Set<String> PATH_SUFFIXES = new HashSet<>(Arrays.asList(
|
||||
"/member/recent/quick-page",
|
||||
"/member/recent/getById",
|
||||
"/member/recent/look-wechat"
|
||||
));
|
||||
|
||||
private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
|
||||
private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
private static final int AES_KEY_BITS = 256;
|
||||
private static final int IV_LEN = 12;
|
||||
|
||||
private final PrivateKey privateKey;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public RespBodyEncryptSupport(String privateKeyBase64, ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
try {
|
||||
byte[] keyBytes = Base64.decode(privateKeyBase64);
|
||||
this.privateKey = KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("初始化响应加密 RSA 私钥失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchPath(String path) {
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return false;
|
||||
}
|
||||
for (String suffix : PATH_SUFFIXES) {
|
||||
if (path.endsWith(suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean wantEncrypt(String encHeader) {
|
||||
return "1".equals(encHeader) || "true".equalsIgnoreCase(encHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 Header 中的 RSA 密文解出 AES Key,再加密 data 为信封 Map。
|
||||
*/
|
||||
public Map<String, Object> encryptData(Object data, String ekBase64) throws Exception {
|
||||
if (StrUtil.isBlank(ekBase64)) {
|
||||
throw new IllegalArgumentException("缺少 X-Resp-Ek");
|
||||
}
|
||||
byte[] aesKey = decryptAesKey(ekBase64);
|
||||
byte[] plain = objectMapper.writeValueAsBytes(data == null ? new HashMap<>() : data);
|
||||
|
||||
byte[] iv = new byte[IV_LEN];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
Cipher aes = Cipher.getInstance(AES_TRANSFORMATION);
|
||||
aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||
byte[] ct = aes.doFinal(plain);
|
||||
|
||||
Map<String, Object> envelope = new HashMap<>(8);
|
||||
envelope.put("enc", 1);
|
||||
envelope.put("v", 1);
|
||||
envelope.put("alg", "RSA-PKCS1-AES-256-GCM");
|
||||
envelope.put("iv", Base64.encode(iv));
|
||||
envelope.put("ct", Base64.encode(ct));
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private byte[] decryptAesKey(String ekBase64) throws Exception {
|
||||
Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
|
||||
rsa.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] key = rsa.doFinal(Base64.decode(ekBase64));
|
||||
if (key.length != 16 && key.length != 24 && key.length != 32) {
|
||||
throw new IllegalArgumentException("AES Key 长度非法: " + key.length);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/** 仅测试/工具用 */
|
||||
public static byte[] generateAesKeyBytes() throws Exception {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(AES_KEY_BITS);
|
||||
SecretKey key = kg.generateKey();
|
||||
return key.getEncoded();
|
||||
}
|
||||
}
|
||||
+38
-1
@@ -33,6 +33,12 @@ import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -60,6 +66,7 @@ import static cn.hutool.core.util.ObjectUtil.notEqual;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.fenToYuanStr;
|
||||
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
|
||||
@@ -90,6 +97,8 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
private PayOrderApi payOrderApi;
|
||||
@Resource
|
||||
private PayRefundApi payRefundApi;
|
||||
@Resource
|
||||
private TradeBrokerageRecordApi tradeBrokerageRecordApi;
|
||||
|
||||
/**
|
||||
* 获取最后一条会员开通记录
|
||||
@@ -423,10 +432,36 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
// 添加会员tag
|
||||
memberRecentTagService.createOrUpdateVipTag(openTypeUserDO.getMemberId());
|
||||
|
||||
// 添加会员开通分销佣金(每次开通都分,对应 OPEN_TYPE)
|
||||
addOpenTypeBrokerage(openTypeUserDO, openType, payOrder);
|
||||
|
||||
String message = String.format("恭喜您购买成功,此次购买的产品:%s", openType.getTitle());
|
||||
this.chatApi.messageUserSendText(openTypeUserDO.getMemberId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员开通支付成功后,给上级增加分销佣金
|
||||
*/
|
||||
private void addOpenTypeBrokerage(OpenTypeUserDO openTypeUserDO, OpenTypeDO openType, PayOrderRespDTO payOrder) {
|
||||
Integer basePrice = payOrder.getPrice();
|
||||
if (basePrice == null || basePrice <= 0) {
|
||||
log.info("[会员开通分佣][开通单({}) 支付金额为0,跳过分佣]", openTypeUserDO.getId());
|
||||
return;
|
||||
}
|
||||
MemberUserDO user = memberUserService.getUser(openTypeUserDO.getMemberId());
|
||||
String nickname = user != null && user.getNickname() != null ? user.getNickname() : String.valueOf(openTypeUserDO.getMemberId());
|
||||
String productTitle = openType != null && openType.getTitle() != null ? openType.getTitle() : "会员";
|
||||
List<BrokerageAddDTO> brokerageList = new ArrayList<>();
|
||||
brokerageList.add(new BrokerageAddDTO()
|
||||
.setBizId(String.valueOf(openTypeUserDO.getId()))
|
||||
.setBasePrice(basePrice)
|
||||
.setFirstFixedPrice(0)
|
||||
.setSecondFixedPrice(0)
|
||||
.setSourceUserId(openTypeUserDO.getMemberId())
|
||||
.setTitle(String.format("用户[%s]开通会员[%s],支付[%s]元", nickname, productTitle, fenToYuanStr(basePrice))));
|
||||
tradeBrokerageRecordApi.addBrokerage(openTypeUserDO.getMemberId(), BrokerageRecordBizTypeEnum.OPEN_TYPE, brokerageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款的回调
|
||||
*/
|
||||
@@ -445,6 +480,8 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
updateObj.setRefundStatus(SUCCESS.getStatus())
|
||||
.setRefundTime(payRefund.getSuccessTime())
|
||||
.setRefundPayPrice(openTypeUserDO.getPayPrice());
|
||||
// 退款成功后作废对应会员佣金
|
||||
tradeBrokerageRecordApi.cancelBrokerage(BrokerageRecordBizTypeEnum.OPEN_TYPE, String.valueOf(id));
|
||||
}
|
||||
// 退款失败
|
||||
if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
|
||||
@@ -452,7 +489,7 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
}
|
||||
// 更新会员开通的退款状态
|
||||
openTypeUserMapper.update(updateObj, new LambdaQueryWrapperX<OpenTypeUserDO>()
|
||||
.eq(OpenTypeUserDO::getId, openTypeUserDO.getOpenTypeId())
|
||||
.eq(OpenTypeUserDO::getId, id)
|
||||
.eq(OpenTypeUserDO::getRefundStatus, WAITING.getStatus())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,11 @@ wx:
|
||||
yudao:
|
||||
captcha:
|
||||
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
|
||||
member:
|
||||
resp-encrypt:
|
||||
enabled: true
|
||||
# RSA PKCS#8 Base64(无 PEM 头尾),与 friend/assets/crypto/resp_enc_public.pem 配对
|
||||
private-key-base64: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w
|
||||
security:
|
||||
mock-enable: true
|
||||
pay:
|
||||
|
||||
@@ -189,6 +189,11 @@ wx:
|
||||
yudao:
|
||||
captcha:
|
||||
enable: true # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
|
||||
member:
|
||||
resp-encrypt:
|
||||
enabled: true
|
||||
# 生产建议用环境变量 YUDAO_RESP_ENCRYPT_PRIVATE_KEY;此处可临时写死与 local 同钥
|
||||
private-key-base64: ${YUDAO_RESP_ENCRYPT_PRIVATE_KEY:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1K3mUuyaWg6eEUd2Hw0lrUvvpT/JBZf5LZU/LJeCnv/1M1xs6XKBDSIPT5Jd3zbzCtWR4ejrsQu1ywsX/tJ15DWbKS/ESzAqEAbtIKNqhyr1LAydKt7uIeeu2c0pFEfxKjN2zruxEk94/Pyjxb9mvpcp4S5K29ncTqfYcPtSDjnmojIUwMmdjue1vcARacZ2FewX02qLbbK6omstA4ykFa7+lMWGkI+vYQJ0x26Ubj3Un6ydH9Rglm7tGjrk7gVvd3gRUL0QcofZxAMsCRVZx4WWJi+KGu61i8GSD1rKC3kVpBmJSt+J7SJXLkW9kuQ7ee/5+b2U9Mo3Jp09vbqQPAgMBAAECggEAHQndYH4I5touWRKoH/EozPeNoi1qxpm7A0m3Wvf0xN6Mkqrt1dJv66jtXGLI9m53IXxioL7yv9jQZ3MZum0wSm1f+u/mMZ2b1+HRK3VE6ebtpV7ZhuvhNrRYGDPthRmnZymiBYF1pF5RIrEUdnJ3oWRBCjwOxZ3Go1O8O3XEqVtBUKPpBgoUQqzFzyeck3HKNCZH62eIdTUQnyp7cU6wyj/9NwOerF7RbRRRFuayPzdPuHJVcye7/qX/4z8GlNLklP1QOXBg4Qj8x48doR8OaFwSrhoDjcjVQ+mdmoTPGcgLDtZSnTiWtY3Z8czovKIFLdtF6Rg0L8P+2ovBnlwMDQKBgQDaFxlnfVXu30SJJSwX6FgIdkRg/VjyuqAVo7T30qmRWENiyhfJ0MhMiiy57tkT0MwQOk3M/+Cp0ah4RTJSOanEr6kttTd3FzjVlbjNmJML6PjLw0UCGxBbdKLVqiqiGfz86MTDn5c+NluIMJ8oZqQT6algUuDbJ9K5hfsNk8bnuwKBgQDUqW/byjSl7Ph+SHOfji2THHvLRdKHQL+WijbOc7FiIsZuQZ5YyqIq7Rbed7dq38Nb9zo9hk4vd5FgcFvI/hPZUy3TngPYJfwvO+mvAdZGBCBY5V5waywhD+BRGrT7FVJ89iCQANPzCx4ficsKNiC7QTzUiCM3jbMD1aZIUDo9vQKBgQCiAtjnbU9iFVAHhaq80+hZ2OmxF2N1Zbg4exWbqE45X9UnVTPYw5WYbmkNcual9s0xHO7g5BDUdqBtQBJCzP1V1JycDOv4NzMMT54qdTZ/eYEhdZ0VFF/RYNL6YOAs7Ztnkc9h93NWTvEuOdhyHdWzyTf/C3Jou6uqEGLQ0j/HswKBgHC2SRoVxue2fFH6NoX3hkI1U4t+O0ICWEkCgl/Tw80+nysqzjFDXMYhmQBaNTWI2XMxyoV3XJMd7UhO5ObXe0+xHVXOpzz1jP6/43AlKhs77qUPFInC08XGM17pJ8j4GRswHDwWpD9pwhJ3Js0HuTtmLUZ7zSaRjLLYluQ22VeFAoGBAKz9Yd7/FfiaJ3RCG585E4WJg+BoO7uBw1xuTjKabt6gxTD3TcSAmnQEZrKSlmzxoF7ypdOUymJU46TzN5KD1MBkLADN+mr2Xk1TtawORFZk0KfZsXuVDwtSMXrmq5nj1Ul8noq8Azz8qTxxcuW4pdry0NF+Wfjny8ATyFDKY08w}
|
||||
security:
|
||||
mock-enable: true
|
||||
pay:
|
||||
|
||||
@@ -235,6 +235,11 @@ yudao:
|
||||
info:
|
||||
version: 1.0.0
|
||||
base-package: cn.iocoder.yudao
|
||||
member:
|
||||
resp-encrypt:
|
||||
# 默认关;local/prod 配置私钥后开启。客户端带 X-Resp-Enc/X-Resp-Ek 时加密 3 个敏感接口 data
|
||||
enabled: true
|
||||
private-key-base64: ${YUDAO_RESP_ENCRYPT_PRIVATE_KEY:}
|
||||
web:
|
||||
admin-ui:
|
||||
url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址
|
||||
|
||||
Reference in new issue
Block a user