Compare commits
4 Commits
0d4b1d18be
...
e888da137e
| Author | SHA1 | Date |
|---|---|---|
|
|
e888da137e | |
|
|
e2ab5bcb13 | |
|
|
9c0f533aff | |
|
|
9705c4f472 |
|
|
@ -167,7 +167,8 @@ mybatis-plus:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
cache-enabled: true
|
cache-enabled: true
|
||||||
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
|
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||||
|
|
||||||
# PageHelper分页插件
|
# PageHelper分页插件
|
||||||
pagehelper:
|
pagehelper:
|
||||||
|
|
@ -240,6 +241,7 @@ volcengine:
|
||||||
callbackUrl: http://47.86.170.114:8011/api/ai/volcCallback
|
callbackUrl: http://47.86.170.114:8011/api/ai/volcCallback
|
||||||
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
|
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
|
||||||
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
|
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
|
||||||
|
projectAesKeyBase64: "gJajABVfQJ9xA94Q9IvQi68fqqhSIkfcKlG7pjGFt2U="
|
||||||
url: https://ark.ap-southeast.bytepluses.com/api/v3
|
url: https://ark.ap-southeast.bytepluses.com/api/v3
|
||||||
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
|
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
|
||||||
callBackUrl: http://47.86.170.114:5173/
|
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.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.*;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import com.ruoyi.ai.mapper.AiUserMapper;
|
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.AiUser;
|
||||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
|
@ -12,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
|
|
@ -20,9 +20,6 @@ import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.security.MessageDigest;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
@ -38,6 +35,8 @@ public class BaseByteApiService {
|
||||||
private AiUserMapper userMapper;
|
private AiUserMapper userMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private SysDeptMapper deptMapper;
|
private SysDeptMapper deptMapper;
|
||||||
|
@Resource
|
||||||
|
private EncryptionService encryptionService;
|
||||||
protected String DEPT_ANCESTORS_SPLIT = ",";
|
protected String DEPT_ANCESTORS_SPLIT = ",";
|
||||||
@Value("${volcengine.ak}")
|
@Value("${volcengine.ak}")
|
||||||
protected String accessKeyId;
|
protected String accessKeyId;
|
||||||
|
|
@ -88,7 +87,7 @@ public class BaseByteApiService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId);
|
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId);
|
||||||
return secondDept.getProject();
|
return encryptionService.decode(secondDept.getProject());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -126,6 +125,7 @@ public class BaseByteApiService {
|
||||||
.header("Authorization", signed.authorization)
|
.header("Authorization", signed.authorization)
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build();
|
.build();
|
||||||
|
log.info("向火山发送请求, body = {}", body);
|
||||||
try (Response response = httpClient.newCall(okRequest).execute()) {
|
try (Response response = httpClient.newCall(okRequest).execute()) {
|
||||||
if (responseClass == null) {
|
if (responseClass == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -134,6 +134,7 @@ public class BaseByteApiService {
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
throw new IOException("HTTP " + response.code() + ": " + respBody);
|
throw new IOException("HTTP " + response.code() + ": " + respBody);
|
||||||
}
|
}
|
||||||
|
log.info("返回结果, respBody = {}", respBody);
|
||||||
return parseResult(respBody, responseClass);
|
return parseResult(respBody, responseClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,13 +196,6 @@ public class BaseByteApiService {
|
||||||
return new SignedRequest(url, xContentSha256, xDate, authorization);
|
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 {
|
private static String canonicalQueryString(Map<String, Object> params) throws IOException {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (String key : new TreeMap<>(params).keySet()) {
|
for (String key : new TreeMap<>(params).keySet()) {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
package com.ruoyi.ai.service.impl;
|
package com.ruoyi.ai.service.impl;
|
||||||
|
|
||||||
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
|
||||||
import com.ruoyi.ai.service.IAiUserService;
|
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.AiUser;
|
||||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||||
import com.ruoyi.common.core.redis.RedisCache;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.system.service.ISysDeptService;
|
import com.ruoyi.system.service.ISysDeptService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
|
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_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_DEPT_ROW_MSG = "用户所属部门不存在或已删除,请核对 ai_user.dept_id";
|
||||||
private static final String NO_API_KEY_MSG = "部门未配置火山 API Key:请在 sys_dept 为用户所在部门(或其上级二级部门)配置 byte_api_key";
|
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 cacheKey = aiUserId + "_byte_api_key";
|
||||||
String cached = redisCache.getCacheObject(cacheKey);
|
String cached = redisCache.getCacheObject(cacheKey);
|
||||||
if (StringUtils.isNotEmpty(cached)) {
|
if (StringUtils.isNotEmpty(cached)) {
|
||||||
return cached;
|
// 解密:缓存保存加密后的字符串比较安全
|
||||||
|
return encryptionService.decode(cached);
|
||||||
}
|
}
|
||||||
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
|
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
|
||||||
if (aiUser == null || aiUser.getDeptId() == null) {
|
if (aiUser == null || aiUser.getDeptId() == null) {
|
||||||
|
|
@ -62,8 +69,10 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
|
||||||
if (StringUtils.isEmpty(apiKey)) {
|
if (StringUtils.isEmpty(apiKey)) {
|
||||||
throw new ServiceException(NO_API_KEY_MSG);
|
throw new ServiceException(NO_API_KEY_MSG);
|
||||||
}
|
}
|
||||||
|
// 缓存中保存加密值
|
||||||
redisCache.setCacheObject(cacheKey, apiKey, CACHE_HOURS, TimeUnit.HOURS);
|
redisCache.setCacheObject(cacheKey, apiKey, CACHE_HOURS, TimeUnit.HOURS);
|
||||||
return apiKey;
|
// 返回处解密
|
||||||
|
return encryptionService.decode(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String trimKey(String raw) {
|
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