feat: 部门表中的项目与apikey加密
This commit is contained in:
parent
eb5211b9e4
commit
9705c4f472
|
|
@ -240,6 +240,7 @@ volcengine:
|
|||
callbackUrl: http://47.86.170.114:8011/api/ai/volcCallback
|
||||
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
|
||||
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
|
||||
projectAesKeyBase64: "gJajABVfQJ9xA94Q9IvQi68fqqhSIkfcKlG7pjGFt2U="
|
||||
url: https://ark.ap-southeast.bytepluses.com/api/v3
|
||||
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
|
||||
callBackUrl: http://47.86.170.114:5173/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.ruoyi.common.utils.crypto;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES-256-CBC PKCS5 加/解密成对使用。密文存库形态:整段 Base64( IV(16字节) ‖ 密文 ),无前缀。
|
||||
*/
|
||||
public final class AesCbcPkcs5Decrypt {
|
||||
|
||||
private static final int IV_LENGTH = 16;
|
||||
private static final int AES_KEY_LENGTH = 32;
|
||||
private static final String AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private AesCbcPkcs5Decrypt() {
|
||||
}
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 使用随机 IV 加密明文,输出与 {@link #decrypt(String, byte[])} 输入格式一致。
|
||||
*
|
||||
* @param plainText UTF-8 明文;{@code null} 非法
|
||||
* @param aes256Key 32 字节 AES-256 密钥
|
||||
* @return Base64( IV ‖ ciphertext ),无换行
|
||||
*/
|
||||
public static String encrypt(String plainText, byte[] aes256Key) throws GeneralSecurityException {
|
||||
if (plainText == null) {
|
||||
throw new IllegalArgumentException("plain text must not be null");
|
||||
}
|
||||
if (aes256Key == null || aes256Key.length != AES_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("aes key must be 32 bytes");
|
||||
}
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
SECURE_RANDOM.nextBytes(iv);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(aes256Key, "AES");
|
||||
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
|
||||
byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] combined = new byte[IV_LENGTH + cipherBytes.length];
|
||||
System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
|
||||
System.arraycopy(cipherBytes, 0, combined, IV_LENGTH, cipherBytes.length);
|
||||
return Base64.getEncoder().encodeToString(combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param base64IvAndCipher Base64( IV ‖ ciphertext )
|
||||
* @param aes256Key 32 字节 AES-256 密钥
|
||||
* @return UTF-8 明文
|
||||
*/
|
||||
public static String decrypt(String base64IvAndCipher, byte[] aes256Key) throws GeneralSecurityException {
|
||||
if (base64IvAndCipher == null || base64IvAndCipher.isEmpty()) {
|
||||
throw new IllegalArgumentException("empty payload");
|
||||
}
|
||||
if (aes256Key == null || aes256Key.length != AES_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("aes key must be 32 bytes");
|
||||
}
|
||||
byte[] combined = Base64.getDecoder().decode(base64IvAndCipher.trim());
|
||||
if (combined.length <= IV_LENGTH) {
|
||||
throw new GeneralSecurityException("payload too short after base64 decode");
|
||||
}
|
||||
byte[] iv = Arrays.copyOfRange(combined, 0, IV_LENGTH);
|
||||
byte[] ciphertext = Arrays.copyOfRange(combined, IV_LENGTH, combined.length);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(aes256Key, "AES");
|
||||
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
|
||||
byte[] plain = cipher.doFinal(ciphertext);
|
||||
return new String(plain, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.ai.service.impl;
|
|||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.ruoyi.ai.mapper.AiUserMapper;
|
||||
import com.ruoyi.common.EncryptionService;
|
||||
import com.ruoyi.common.core.domain.entity.AiUser;
|
||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
|
|
@ -12,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import okhttp3.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import javax.crypto.Mac;
|
||||
|
|
@ -20,9 +20,6 @@ import javax.crypto.spec.SecretKeySpec;
|
|||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
|
@ -38,6 +35,8 @@ public class BaseByteApiService {
|
|||
private AiUserMapper userMapper;
|
||||
@Resource
|
||||
private SysDeptMapper deptMapper;
|
||||
@Resource
|
||||
private EncryptionService encryptionService;
|
||||
protected String DEPT_ANCESTORS_SPLIT = ",";
|
||||
@Value("${volcengine.ak}")
|
||||
protected String accessKeyId;
|
||||
|
|
@ -88,7 +87,7 @@ public class BaseByteApiService {
|
|||
return null;
|
||||
}
|
||||
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId);
|
||||
return secondDept.getProject();
|
||||
return encryptionService.decode(secondDept.getProject());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -195,13 +194,6 @@ public class BaseByteApiService {
|
|||
return new SignedRequest(url, xContentSha256, xDate, authorization);
|
||||
}
|
||||
|
||||
private static String esc(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private static String canonicalQueryString(Map<String, Object> params) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String key : new TreeMap<>(params).keySet()) {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
package com.ruoyi.ai.service.impl;
|
||||
|
||||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||
import com.ruoyi.ai.service.IAiUserService;
|
||||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||
import com.ruoyi.common.EncryptionService;
|
||||
import com.ruoyi.common.core.domain.entity.AiUser;
|
||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.service.ISysDeptService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
|
||||
|
||||
@Resource
|
||||
private EncryptionService encryptionService;
|
||||
private static final String NO_DEPT_MSG = "用户未分配部门:请在后台为门户用户设置 ai_user.dept_id(关联 sys_dept.dept_id)";
|
||||
private static final String NO_DEPT_ROW_MSG = "用户所属部门不存在或已删除,请核对 ai_user.dept_id";
|
||||
private static final String NO_API_KEY_MSG = "部门未配置火山 API Key:请在 sys_dept 为用户所在部门(或其上级二级部门)配置 byte_api_key";
|
||||
|
|
@ -38,7 +44,8 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
|
|||
String cacheKey = aiUserId + "_byte_api_key";
|
||||
String cached = redisCache.getCacheObject(cacheKey);
|
||||
if (StringUtils.isNotEmpty(cached)) {
|
||||
return cached;
|
||||
// 解密:缓存保存加密后的字符串比较安全
|
||||
return encryptionService.decode(cached);
|
||||
}
|
||||
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
|
||||
if (aiUser == null || aiUser.getDeptId() == null) {
|
||||
|
|
@ -62,8 +69,10 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
|
|||
if (StringUtils.isEmpty(apiKey)) {
|
||||
throw new ServiceException(NO_API_KEY_MSG);
|
||||
}
|
||||
// 缓存中保存加密值
|
||||
redisCache.setCacheObject(cacheKey, apiKey, CACHE_HOURS, TimeUnit.HOURS);
|
||||
return apiKey;
|
||||
// 返回处解密
|
||||
return encryptionService.decode(apiKey);
|
||||
}
|
||||
|
||||
private static String trimKey(String raw) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.ruoyi.common;
|
||||
|
||||
import com.ruoyi.common.utils.crypto.AesCbcPkcs5Decrypt;
|
||||
import com.ruoyi.common.utils.http.OkHttpUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 加解密服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EncryptionService {
|
||||
/** Base64 编码的 32 字节 AES-256 密钥;空表示不对 project 字段解密 */
|
||||
@Value("${volcengine.projectAesKeyBase64}")
|
||||
private String projectAesKeyBase64;
|
||||
|
||||
/** 解析后的密钥;未配置或非法则为 null */
|
||||
private byte[] projectAesKeyBytes;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.projectAesKeyBytes = decodeProjectAesKey(projectAesKeyBase64);
|
||||
}
|
||||
|
||||
private byte[] decodeProjectAesKey(String base64Key) {
|
||||
if (base64Key == null || base64Key.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] k = Base64.getDecoder().decode(base64Key.trim());
|
||||
if (k.length != 32) {
|
||||
log.error("volcengine.projectAesKeyBase64 must decode to 32 bytes, got {}", k.length);
|
||||
return null;
|
||||
}
|
||||
return k;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("invalid volcengine.projectAesKeyBase64", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public String decode(String value){
|
||||
if (value == null || value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
if (projectAesKeyBytes == null) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return AesCbcPkcs5Decrypt.decrypt(value.trim(), projectAesKeyBytes);
|
||||
} catch (GeneralSecurityException | IllegalArgumentException e) {
|
||||
log.warn("project field AES decrypt failed, using raw value: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String encode(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
if (projectAesKeyBytes == null) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return AesCbcPkcs5Decrypt.encrypt(value.trim(), projectAesKeyBytes);
|
||||
} catch (GeneralSecurityException | IllegalArgumentException e) {
|
||||
log.warn("project field AES encode failed, using raw value: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue