RSA接口加密封装优化

v1.4.1
Parker 4 years ago
parent 9885b962ea
commit 182e1ce709

@ -0,0 +1,28 @@
package org.opsli.api.base.encrypt;
import com.alibaba.excel.annotation.ExcelIgnore;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
*
*
* @author
* @date 2021-01-24 12:48
**/
@Data
public class BaseEncrypt implements Serializable {
private static final long serialVersionUID = 1L;
/** 加密数据 */
@ApiModelProperty(value = "加密数据")
@ExcelIgnore
@TableField(exist = false)
private String encryptData;
}

@ -28,6 +28,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.apache.poi.ss.usermodel.FillPatternType; import org.apache.poi.ss.usermodel.FillPatternType;
import org.opsli.api.base.encrypt.BaseEncrypt;
import org.opsli.plugins.excel.annotation.ExcelInfo; import org.opsli.plugins.excel.annotation.ExcelInfo;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@ -57,7 +58,7 @@ import java.util.Date;
@HeadFontStyle(fontName = "Arial",color = 9,fontHeightInPoints = 10) @HeadFontStyle(fontName = "Arial",color = 9,fontHeightInPoints = 10)
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 23) @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 23)
@ColumnWidth(22) @ColumnWidth(22)
public abstract class ApiWrapper implements Serializable { public abstract class ApiWrapper extends BaseEncrypt implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

@ -0,0 +1,24 @@
package org.opsli.common.annotation;
import java.lang.annotation.*;
/**
*
* @author Parker
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InterfaceEncryptAndDecrypt {
/** 加密启用状态 */
boolean enable() default true;
/** 请求解密 */
boolean requestDecrypt() default true;
/** 返回加密 */
boolean responseEncrypt() default true;
}

@ -31,6 +31,9 @@ public interface OrderConstants {
/** token */ /** token */
int TOKEN_AOP_SORT = 150; int TOKEN_AOP_SORT = 150;
/** 请求加解密 */
int ENCRYPT_ADN_DECRYPT_AOP_SORT = 160;
/** 热点数据加载顺序 */ /** 热点数据加载顺序 */
int HOT_DATA_ORDER = 180; int HOT_DATA_ORDER = 180;

@ -119,12 +119,10 @@ public final class RateLimiterUtil {
RateLimiterInner rateLimiterInner = rateLimiterInnerMap.get(resource); RateLimiterInner rateLimiterInner = rateLimiterInnerMap.get(resource);
// 如果为空 则创建一个新的限流器 // 如果为空 则创建一个新的限流器
if(rateLimiterInner == null){ if(rateLimiterInner == null){
System.out.println(456);
rateLimiterInner = new RateLimiterInner(); rateLimiterInner = new RateLimiterInner();
rateLimiterInner.setQps(dfQps); rateLimiterInner.setQps(dfQps);
rateLimiterInner.setRateLimiter(RateLimiter.create(dfQps)); rateLimiterInner.setRateLimiter(RateLimiter.create(dfQps));
rateLimiterInnerMap.put(resource, rateLimiterInner); rateLimiterInnerMap.put(resource, rateLimiterInner);
}else{ }else{
qps = rateLimiterInner.getQps(); qps = rateLimiterInner.getQps();
} }

@ -0,0 +1,168 @@
/**
* Copyright 2020 OPSLI https://www.opsli.com
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.opsli.core.aspect;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import cn.hutool.crypto.asymmetric.RSA;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.opsli.api.base.encrypt.BaseEncrypt;
import org.opsli.api.base.result.ResultVo;
import org.opsli.common.annotation.InterfaceEncryptAndDecrypt;
import org.opsli.common.exception.ServiceException;
import org.opsli.common.utils.Props;
import org.opsli.core.msg.CoreMsg;
import org.opsli.core.utils.EncryptAndDecryptByRsaUtil;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import static org.opsli.common.constants.OrderConstants.ENCRYPT_ADN_DECRYPT_AOP_SORT;
/**
*
*
* @author parker
* @date 2021-01-23
*/
@Slf4j
@Order(ENCRYPT_ADN_DECRYPT_AOP_SORT)
@Aspect
@Component
public class InterfaceEncryptAndDecryptAop {
/** RSA 公钥 */
private static String RSA_PUBLIC_KEY;
/** RSA 私钥 */
private static String RSA_PRIVATE_KEY;
/** RSA */
private static RSA ASSIGN_RSA;
static {
// 缓存前缀
Props props = new Props("application.yaml");
RSA_PUBLIC_KEY = props.getStr("opsli.encrypt-decrypt.rsa.public-key");
RSA_PRIVATE_KEY = props.getStr("opsli.encrypt-decrypt.rsa.private-key");
try {
ASSIGN_RSA = EncryptAndDecryptByRsaUtil.INSTANCE.createRsa(RSA_PUBLIC_KEY, RSA_PRIVATE_KEY);
}catch (Exception e){
ASSIGN_RSA = EncryptAndDecryptByRsaUtil.INSTANCE.createRsa();
RSA_PUBLIC_KEY = ASSIGN_RSA.getPublicKeyBase64();
RSA_PRIVATE_KEY = ASSIGN_RSA.getPrivateKeyBase64();
String errorMsg = StrUtil.format(CoreMsg.OTHER_EXCEPTION_RSA_CREATE.getMessage(),
RSA_PUBLIC_KEY, RSA_PRIVATE_KEY
);
log.error(errorMsg);
}
}
/**
*
* @return
*/
public static String getRsaPublicKey() {
return RSA_PUBLIC_KEY;
}
/**
*
* @return
*/
public static String getRsaPrivateKey() {
return RSA_PRIVATE_KEY;
}
@Pointcut("@annotation(org.opsli.common.annotation.InterfaceEncryptAndDecrypt)")
public void encryptAndDecrypt() {
}
/**
* post
* @param point
*/
@Around("encryptAndDecrypt()")
public Object encryptAndDecryptHandle(ProceedingJoinPoint point) throws Throwable {
// 获得请求参数
Object[] args = point.getArgs();
// 返回结果
Object returnValue = null;
MethodSignature signature = (MethodSignature) point.getSignature();
// 获得 方法
Method method = signature.getMethod();
// 获得方法注解
InterfaceEncryptAndDecrypt annotation =
method.getAnnotation(InterfaceEncryptAndDecrypt.class);
if(annotation != null){
// 1. 拆解请求数据
// request 解密
if (annotation.enable() && annotation.requestDecrypt()){
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
// 参数校验
if(arg instanceof BaseEncrypt){
// 获得加密数据
BaseEncrypt baseEncrypt = (BaseEncrypt) arg;
String encryptData = baseEncrypt.getEncryptData();
// 解密对象
Object dataToObj = EncryptAndDecryptByRsaUtil.INSTANCE.decryptedDataToObj(ASSIGN_RSA, encryptData);
// 根据方法类型转化对象
Type type = TypeUtil.getParamType(method, i);
args[i] = Convert.convert(type, dataToObj);
}
}
}
// 2. 执行方法
returnValue = point.proceed(args);
// 3. 返回响应数据
// response 加密
if (annotation.enable() && annotation.responseEncrypt()){
try {
// 执行加密过程
if(returnValue instanceof ResultVo){
ResultVo<Object> ret = (ResultVo<Object>) returnValue;
ret.setData(
EncryptAndDecryptByRsaUtil.INSTANCE.encryptedData(
ASSIGN_RSA, ret.getData()
)
);
returnValue = ret;
}else {
returnValue = EncryptAndDecryptByRsaUtil.INSTANCE.encryptedData(
ASSIGN_RSA, returnValue
);
}
}catch (Exception e){
// RSA非对称加密失败
throw new ServiceException(CoreMsg.OTHER_EXCEPTION_RSA_EN);
}
}
}
return returnValue;
}
}

@ -69,9 +69,10 @@ public enum CoreMsg implements BaseMsg {
/** 其他 */ /** 其他 */
OTHER_EXCEPTION_LIMITER(10700,"当前系统繁忙,请稍后再试"), OTHER_EXCEPTION_LIMITER(10700,"当前系统繁忙,请稍后再试"),
OTHER_EXCEPTION_RSA_EN(10701,"RSA非对称加密失败"), OTHER_EXCEPTION_RSA_CREATE(10701,"指定RSA算法器创建失败系统已自动创建随机RSA算法\n公钥{}\n私钥{}"),
OTHER_EXCEPTION_RSA_DE(10701,"RSA非对称解密失败"), OTHER_EXCEPTION_RSA_EN(10702,"RSA非对称加密失败"),
OTHER_EXCEPTION_RSA_REFLEX(10702,"RSA非对称解密反射失败"), OTHER_EXCEPTION_RSA_DE(10703,"RSA非对称解密失败"),
OTHER_EXCEPTION_RSA_REFLEX(10704,"RSA非对称解密反射失败"),
; ;

@ -22,6 +22,7 @@ import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA; import cn.hutool.crypto.asymmetric.RSA;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import org.opsli.common.exception.ServiceException; import org.opsli.common.exception.ServiceException;
import org.opsli.common.utils.Props;
import org.opsli.core.msg.CoreMsg; import org.opsli.core.msg.CoreMsg;
import java.util.Collection; import java.util.Collection;
@ -31,10 +32,10 @@ import java.util.Collection;
* *
* @author * @author
*/ */
public enum AsymmetricCryptoUtil { public enum EncryptAndDecryptByRsaUtil {
/** 实例 */ /** 默认实例 */
INSTANCE; INSTANCE();
/** RSA KEY */ /** RSA KEY */
private final String rsaKey = "data"; private final String rsaKey = "data";
@ -42,11 +43,11 @@ public enum AsymmetricCryptoUtil {
/** RSA对象 */ /** RSA对象 */
private final RSA rsa; private final RSA rsa;
AsymmetricCryptoUtil(){ EncryptAndDecryptByRsaUtil(){
// 初始化RSA对象
this.rsa = this.createRsa(); this.rsa = this.createRsa();
} }
/** /**
* RSA * RSA
* @return RSA * @return RSA
@ -74,6 +75,14 @@ public enum AsymmetricCryptoUtil {
return rsa.getPublicKeyBase64(); return rsa.getPublicKeyBase64();
} }
/**
*
* @return String
*/
public String getPrivateKey(){
return rsa.getPrivateKeyBase64();
}
/** /**
* RSA * RSA
* @param data * @param data

@ -3,8 +3,10 @@ import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import lombok.Data; import lombok.Data;
import org.junit.Test; import org.junit.Test;
import org.opsli.core.utils.AsymmetricCryptoUtil; import org.opsli.core.utils.EncryptAndDecryptByRsaUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -18,7 +20,7 @@ public class RsaTest {
*/ */
@Test @Test
public void getPublicKey(){ public void getPublicKey(){
System.out.println(AsymmetricCryptoUtil.INSTANCE.getPublicKey()); System.out.println(EncryptAndDecryptByRsaUtil.INSTANCE.getPublicKey());
} }
@ -36,18 +38,24 @@ public class RsaTest {
// t1.setName( "张三"); // t1.setName( "张三");
// t1.setAge( 16); // t1.setAge( 16);
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(111);
Object parse = JSONObject.parse("{\"username\":\"demo\",\"password\":\"Aa123456\",\"captcha\":\"\",\"uuid\":\"0d3eea43edf19e4ed0e88aae8d56878046a5\"}"); Object parse = JSONObject.parse("{\"username\":\"demo\",\"password\":\"Aa123456\",\"captcha\":\"\",\"uuid\":\"0d3eea43edf19e4ed0e88aae8d56878046a5\"}");
// 加密 // 加密
String encryptedData = AsymmetricCryptoUtil.INSTANCE.encryptedData(parse); String encryptedData = EncryptAndDecryptByRsaUtil.INSTANCE.encryptedData(list);
System.out.println(encryptedData); System.out.println(encryptedData);
// 解密 // 解密
String decryptedData = AsymmetricCryptoUtil.INSTANCE.decryptedData(encryptedData); String decryptedData = EncryptAndDecryptByRsaUtil.INSTANCE.decryptedData(encryptedData);
Object decryptedDataToObj = AsymmetricCryptoUtil.INSTANCE.decryptedDataToObj(encryptedData); Object decryptedDataToObj = EncryptAndDecryptByRsaUtil.INSTANCE.decryptedDataToObj(encryptedData);
System.out.println(decryptedData); System.out.println(decryptedData);
// 解密 // 解密
List<Integer> integers = Convert.toList(Integer.class, decryptedDataToObj);
//Map<String, Object> stringObjectMap = Convert.toMap(String.class, Object.class, decryptedData); //Map<String, Object> stringObjectMap = Convert.toMap(String.class, Object.class, decryptedData);
//Map<String, Object> stringObjectMap = Convert.toMap(String.class, Object.class, decryptedDataToObj); //Map<String, Object> stringObjectMap = Convert.toMap(String.class, Object.class, decryptedDataToObj);

@ -18,6 +18,7 @@ package org.opsli.modulars.system.login.entity;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.opsli.api.base.encrypt.BaseEncrypt;
import org.opsli.common.annotation.validation.ValidationArgs; import org.opsli.common.annotation.validation.ValidationArgs;
import org.opsli.common.annotation.validation.ValidationArgsLenMax; import org.opsli.common.annotation.validation.ValidationArgsLenMax;
import org.opsli.common.annotation.validation.ValidationArgsLenMin; import org.opsli.common.annotation.validation.ValidationArgsLenMin;
@ -31,7 +32,7 @@ import org.opsli.common.enums.ValiArgsType;
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
public class LoginForm { public class LoginForm extends BaseEncrypt {
/** 用户名 */ /** 用户名 */
@ApiModelProperty(value = "用户名") @ApiModelProperty(value = "用户名")
@ -52,7 +53,6 @@ public class LoginForm {
/** UUID */ /** UUID */
@ApiModelProperty(value = "UUID") @ApiModelProperty(value = "UUID")
@ValidationArgs(ValiArgsType.IS_NOT_NULL)
private String uuid; private String uuid;
} }

@ -15,8 +15,6 @@
*/ */
package org.opsli.modulars.system.login.web; package org.opsli.modulars.system.login.web;
import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -26,6 +24,7 @@ import org.opsli.api.base.result.ResultVo;
import org.opsli.api.utils.ValidationUtil; import org.opsli.api.utils.ValidationUtil;
import org.opsli.api.wrapper.system.tenant.TenantModel; import org.opsli.api.wrapper.system.tenant.TenantModel;
import org.opsli.api.wrapper.system.user.UserModel; import org.opsli.api.wrapper.system.user.UserModel;
import org.opsli.common.annotation.InterfaceEncryptAndDecrypt;
import org.opsli.common.annotation.Limiter; import org.opsli.common.annotation.Limiter;
import org.opsli.common.api.TokenThreadLocal; import org.opsli.common.api.TokenThreadLocal;
import org.opsli.common.enums.AlertType; import org.opsli.common.enums.AlertType;
@ -33,14 +32,13 @@ import org.opsli.common.exception.TokenException;
import org.opsli.common.thread.refuse.AsyncProcessQueueReFuse; import org.opsli.common.thread.refuse.AsyncProcessQueueReFuse;
import org.opsli.common.utils.IPUtil; import org.opsli.common.utils.IPUtil;
import org.opsli.common.utils.OutputStreamUtil; import org.opsli.common.utils.OutputStreamUtil;
import org.opsli.core.aspect.InterfaceEncryptAndDecryptAop;
import org.opsli.core.msg.TokenMsg; import org.opsli.core.msg.TokenMsg;
import org.opsli.core.security.shiro.realm.JwtRealm; import org.opsli.core.security.shiro.realm.JwtRealm;
import org.opsli.core.utils.*; import org.opsli.core.utils.*;
import org.opsli.modulars.system.login.entity.LoginForm; import org.opsli.modulars.system.login.entity.LoginForm;
import org.opsli.modulars.system.login.entity.LoginFormStr;
import org.opsli.modulars.system.user.service.IUserService; import org.opsli.modulars.system.user.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -67,41 +65,18 @@ import java.util.Map;
@RestController @RestController
public class LoginRestController { public class LoginRestController {
/** 登录是否开启 RSA加密 */
@Value("${opsli.login.login-rsa:false}")
private boolean loginRsa;
@Autowired @Autowired
private IUserService iUserService; private IUserService iUserService;
/** /**
* *
*/ */
@Limiter @Limiter
@InterfaceEncryptAndDecrypt(responseEncrypt = false)
@ApiOperation(value = "登录", notes = "登录") @ApiOperation(value = "登录", notes = "登录")
@PostMapping("/sys/login") @PostMapping("/sys/login")
public ResultVo<?> login(@RequestBody LoginFormStr formStr, HttpServletRequest request){ public ResultVo<?> login(@RequestBody LoginForm form, HttpServletRequest request){
// 非空验证
if(formStr == null || StringUtils.isBlank(formStr.getLoginFormStr())){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL);
}
LoginForm form;
try{
Object loginFromObj;
if(loginRsa){
loginFromObj = AsymmetricCryptoUtil.INSTANCE.decryptedDataToObj(formStr.getLoginFormStr());
}else {
loginFromObj = JSONObject.parse(formStr.getLoginFormStr());
}
form = Convert.convert(LoginForm.class, loginFromObj);
}catch (Exception e){
log.error("登录账号密码解析失败 - 解析值:["+formStr.getLoginFormStr()+"]",e.getMessage(), e);
// 登录账号密码解析失败
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_DECRYPT);
}
// 非空验证 // 非空验证
if(form == null){ if(form == null){
throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL); throw new TokenException(TokenMsg.EXCEPTION_LOGIN_NULL);
@ -110,7 +85,6 @@ public class LoginRestController {
// 验证登录对象 // 验证登录对象
ValidationUtil.verify(form); ValidationUtil.verify(form);
// 判断账号是否临时锁定 // 判断账号是否临时锁定
UserTokenUtil.verifyLockAccount(form.getUsername()); UserTokenUtil.verifyLockAccount(form.getUsername());
@ -234,7 +208,7 @@ public class LoginRestController {
public ResultVo<?> getPublicKey(){ public ResultVo<?> getPublicKey(){
return ResultVo.success( return ResultVo.success(
"操作成功!", "操作成功!",
AsymmetricCryptoUtil.INSTANCE.getPublicKey() InterfaceEncryptAndDecryptAop.getRsaPublicKey()
); );
} }

@ -203,6 +203,14 @@ opsli:
# 失败锁定时间(秒) # 失败锁定时间(秒)
slip-lock-speed: 300 slip-lock-speed: 300
#加解密
encrypt-decrypt:
# RSA 加密算法
rsa:
# 公钥
public-key: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCakyyq8rfkmKvKB2fz3hUeuD6tgParCmZmRc1OoL5EN+yXghQmDIrcZhewLZZLUpLQd3T3cRxKaW30nWNfoteNHgKjGYGu4+BZoyHZ8ltTmrolYGopiSwBMhO7kwAD4IK1PZHaoF2wPISH35ubbugykav7dTaWBDuNkvyBiv8qMQIDAQAB"
# 私钥
private-key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJqTLKryt+SYq8oHZ/PeFR64Pq2A9qsKZmZFzU6gvkQ37JeCFCYMitxmF7AtlktSktB3dPdxHEppbfSdY1+i140eAqMZga7j4FmjIdnyW1OauiVgaimJLAEyE7uTAAPggrU9kdqgXbA8hIffm5tu6DKRq/t1NpYEO42S/IGK/yoxAgMBAAECgYEAiWu+klwm0LxKPdpHuK7/58e1MVst8PHWB6aW2AhgHxX46NlkQE92RGsfNCnTLDPFAkCxZCrTE/SXJJmn9yY2qoS26OV0PbTGajk96M8lDi9JSmWCNV1eywPecObSyvtPd5jaPtq2jkgNY/hHJjH6kV7UAFZuaSK7jxskfq7uR2ECQQDPfmGjPiMc65+LE9U7jC4LokyUi1yCgN6AY5MgF6fkxUVJD2mtl9BqRK7qE0OnsRb0NzID3PSfa7aA2I0Rlsj/AkEAvrXUBQ6hfuEwD1896qpSJUr7tLidby/3jYwSoewuydDT2duDc2ZCz4/U/1NpxSxWT10ZZi2ExsFZn/3PDylczwJARA3oijkcHSUu69eybVh51bkCswnOasNHtwZxv+niWEdXhTH38EbFxcUHNaDh5MNRiwH7dobm+M7EShg8lJNHEwJAclRdU97OkFr9zeliHCGZd4P5XAFlWHfgJ7p2nR4Teqe3qZ6Aspj2qqpmnd7qxOrsn02H4YqeU+0sBs9I56T7XwJAAg8wHrh/FAPY96mAya0bpv6zm/7bave17vs+8B+fhBEHHuvetfv8Xi/RkXL0rjE4LaTHefoUbZPNbIhNYiN0CQ=="
# Excel 最大操作数量 防止OOM # Excel 最大操作数量 防止OOM
excel-max-count: 20000 excel-max-count: 20000

Loading…
Cancel
Save