feat(X-Pack): 支持 Hmac-auth

This commit is contained in:
fit2cloud-chenyw
2026-04-09 16:20:27 +08:00
committed by fit2cloud-chenyw
parent 6fa2373a42
commit b833138e27
11 changed files with 157 additions and 0 deletions

View File

@@ -98,6 +98,10 @@
<key-type>java.lang.String</key-type>
<value-type>java.lang.Object</value-type>
</cache>
<cache alias="de_v2_global_hmac" uses-template="common-cache">
<key-type>java.lang.String</key-type>
<value-type>java.lang.Object</value-type>
</cache>
<cache alias="de_v2_user_community_language" uses-template="common-cache">
<key-type>java.lang.String</key-type>
<value-type>java.lang.Object</value-type>

View File

@@ -0,0 +1,73 @@
import { xpackModelApi } from '@/api/plugin'
import { useCache } from '@/hooks/web/useCache'
import { isNull } from '@/utils/utils'
import { useEmbedded } from '@/store/modules/embedded'
const { wsCache } = useCache()
const embeddedStore = useEmbedded()
const basePath = import.meta.env.VITE_API_BASEPATH
const embeddedBasePath =
basePath.startsWith('./') && basePath.length > 2 ? basePath.substring(2) : basePath
export const PATH_URL = embeddedStore.baseUrl ? embeddedStore?.baseUrl + embeddedBasePath : basePath
const remoteTsUrl = PATH_URL + '/DEXPackTs.umd.js'
const loadXpackTs = (url: string) => {
return new Promise<void>(function (resolve, reject) {
const scriptId = 'de-fit2cloud-script-xpack-ts-id'
let dom = document.getElementById(scriptId)
if (dom) {
dom.parentElement?.removeChild(dom)
dom = null
}
const script = document.createElement('script')
script.id = scriptId
script.onload = function () {
return resolve()
}
script.onerror = function () {
return reject(new Error('Load script from '.concat(url, ' failed')))
}
script.src = url
const head = document.head || document.getElementsByTagName('head')[0]
;(document.body || head).appendChild(script)
})
}
export const importXpackTool = async (methodName: string) => {
const jsname = 'L3Rvb2xzL0htYWNUb29s'
const key = 'xpack-model-distributed'
let distributed = false
if (wsCache.get(key) === null) {
const res = await xpackModelApi()
const resData = isNull(res.data) ? 'null' : res.data
wsCache.set('xpack-model-distributed', resData)
distributed = res.data
} else {
distributed = wsCache.get(key)
}
distributed = true
if (isNull(distributed) || !distributed) {
return null
}
if (window['DEXPackTs']) {
const xpack = await window['DEXPackTs'].mapping[jsname]
return xpack[methodName]
} else {
if (!window['de_hmac_promise']) {
window['de_hmac_promise'] = null
}
if (!window['de_hmac_promise']) {
window['de_hmac_promise'] = loadXpackTs(remoteTsUrl)
.then(() => {
return window['DEXPackTs']
})
.catch((error: any) => {
window['__de_xpack_loading__'] = null
throw error
})
}
await window['de_hmac_promise']
const xpack = await window['DEXPackTs'].mapping[jsname]
return xpack[methodName]
}
}

View File

@@ -0,0 +1,18 @@
import { importXpackTool } from '@/components/plugin/src/ImportXpackTool'
const hmac_white_list = ['/xpackModel']
export const securityConfig = async (config: any, requestPath: string) => {
if (hmac_white_list.some(item => requestPath.includes(item))) {
return
}
try {
const method = await importXpackTool('securityConfig')
if (!method) {
return
}
return method(config, requestPath)
} catch (error) {
console.error('Failed to load securityConfig method:', error)
return
}
}

View File

@@ -17,6 +17,7 @@ import { configHandler } from './refresh'
import { isMobile, getLocale } from '@/utils/utils'
import { useRequestStoreWithOut } from '@/store/modules/request'
import { clearCache } from '@/utils/cacheUtil'
import { securityConfig } from './hmac'
type AxiosErrorWidthLoading<T> = T & {
config: {
@@ -100,6 +101,7 @@ service.interceptors.request.use(
if (config instanceof Promise) {
config = await config
}
await securityConfig(config, service.getUri(config))
if (
config.method === 'post' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===

View File

@@ -4814,6 +4814,10 @@ export default {
security: {
title: 'Security Settings'
},
setting_hmac: {
title: 'HMAC Settings',
enable: 'Enable HMAC Authentication'
},
setting_mfa: {
title: 'MFA Settings',
status: 'Global MFA Authentication Enabled',

View File

@@ -4670,6 +4670,10 @@ export default {
security: {
title: '安全設置'
},
setting_hmac: {
title: 'HMAC 設定',
enable: '啟用 HMAC 認證'
},
setting_mfa: {
title: 'MFA 設置',
status: '全局啟用 MFA 認證',

View File

@@ -4679,6 +4679,10 @@ export default {
security: {
title: '安全设置'
},
setting_hmac: {
title: 'HMAC 设置',
enable: '启用 HMAC 认证'
},
setting_mfa: {
title: 'MFA 设置',
status: '全局启用 MFA 认证',

View File

@@ -41,4 +41,21 @@ public interface PerSettingApi {
@Operation(summary = "查询MFA状态")
@GetMapping("/mfaStatus")
Integer mfaStatus();
@Operation(summary = "查询Hmac设置")
@GetMapping("/hmac/query")
List<PerSettingItemVO> hmacSetting();
@Operation(summary = "保存Hmac设置")
@PostMapping("/hmac/save")
void saveHmac(@RequestBody List<PerSettingItemVO> settings);
@Hidden
@GetMapping("/hmac/info")
String hmacInfo();
@Hidden
@PostMapping("/hmac/refresh")
String refreshHmacSecret();
}

View File

@@ -31,6 +31,7 @@ public class CacheConstant {
public static final String RSA_CACHE = "de_v2_rsa";
public static final String PER_MENU_ID_CACHE = "de_v2_per_menu_id";
public static final String GLOBAL_MFA_CACHE = "de_v2_global_mfa";
public static final String GLOBAL_HMAC_CACHE = "de_v2_global_hmac";
}
public static class LicenseCacheConstant {

View File

@@ -9,6 +9,8 @@ import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AesUtils {
@@ -62,4 +64,31 @@ public class AesUtils {
public static Object aesDecrypt(Object o) {
return o == null ? null : aesDecrypt(o.toString(), "www.fit2cloud.co", "1234567890123456");
}
public static String aesEncryptWithIv(String src, String secretKey) {
if (StringUtils.isBlank(secretKey)) {
throw new RuntimeException("secretKey is empty");
}
try {
// 生成随机IV
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] raw = secretKey.getBytes(UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec);
byte[] encrypted = cipher.doFinal(src.getBytes(UTF_8));
// 将IV拼接到密文前面
byte[] ivAndCipher = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, ivAndCipher, 0, iv.length);
System.arraycopy(encrypted, 0, ivAndCipher, iv.length, encrypted.length);
return Base64.encodeBase64String(ivAndCipher);
} catch (Exception e) {
throw new RuntimeException("AES encrypt error:", e);
}
}
}

View File

@@ -50,6 +50,7 @@ public class WhitelistUtils {
"/embedded/initIframe",
"/sysParameter/i18nOptions",
"/login/modifyInvalidPwd",
"/perSetting/hmac/info",
"/");
public static boolean match(String requestURI) {