diff --git a/common/src/main/java/com/xuqm/common/security/AppRequestSignatureUtil.java b/common/src/main/java/com/xuqm/common/security/AppRequestSignatureUtil.java deleted file mode 100644 index 4363142..0000000 --- a/common/src/main/java/com/xuqm/common/security/AppRequestSignatureUtil.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.xuqm.common.security; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Base64; - -public final class AppRequestSignatureUtil { - - private static final String HMAC_ALG = "HmacSHA256"; - - private AppRequestSignatureUtil() { - } - - public static String payload(String appKey, - String userId, - long timestamp, - String nonce) { - return normalize(appKey) + '\n' - + normalize(userId) + '\n' - + timestamp + '\n' - + normalize(nonce); - } - - public static String sign(String secret, String payload) { - try { - Mac mac = Mac.getInstance(HMAC_ALG); - mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALG)); - byte[] digest = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); - return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); - } catch (Exception e) { - throw new IllegalStateException("Failed to sign app request", e); - } - } - - public static boolean matches(String secret, String payload, String expectedSignature) { - String actual = sign(secret, payload); - return MessageDigest.isEqual( - actual.getBytes(StandardCharsets.UTF_8), - normalize(expectedSignature).getBytes(StandardCharsets.UTF_8) - ); - } - - private static String normalize(String value) { - return value == null ? "" : value; - } -} diff --git a/common/src/main/java/com/xuqm/common/security/AppSignatureAuthFilter.java b/common/src/main/java/com/xuqm/common/security/AppSignatureAuthFilter.java deleted file mode 100644 index cec74c3..0000000 --- a/common/src/main/java/com/xuqm/common/security/AppSignatureAuthFilter.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.xuqm.common.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.time.Instant; - -/** - * 验证 SDK 请求的 HMAC-SHA256 签名。 - *
- * 请求头: - *
- * 签名载荷格式:{@code appKey\nuserId\ntimestamp\nnonce}
- */
-public class AppSignatureAuthFilter extends OncePerRequestFilter {
-
- private static final Logger log = LoggerFactory.getLogger(AppSignatureAuthFilter.class);
- private static final long MAX_TIMESTAMP_DRIFT_SECONDS = 300; // ±5 分钟
-
- private final AppSecretResolver secretResolver;
-
- public AppSignatureAuthFilter(AppSecretResolver secretResolver) {
- this.secretResolver = secretResolver;
- }
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
- FilterChain filterChain) throws ServletException, IOException {
- String appKey = request.getHeader("X-App-Key");
- String timestampStr = request.getHeader("X-Timestamp");
- String nonce = request.getHeader("X-Nonce");
- String signature = request.getHeader("X-Signature");
-
- // 如果没有签名头,跳过验证(交给后续 Filter 处理)
- if (appKey == null || signature == null) {
- filterChain.doFilter(request, response);
- return;
- }
-
- // 验证时间戳
- if (timestampStr == null || timestampStr.isBlank()) {
- log.warn("Missing X-Timestamp header for appKey={}", appKey);
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing timestamp");
- return;
- }
-
- long timestamp;
- try {
- timestamp = Long.parseLong(timestampStr);
- } catch (NumberFormatException e) {
- log.warn("Invalid X-Timestamp header: {} for appKey={}", timestampStr, appKey);
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid timestamp");
- return;
- }
-
- long now = Instant.now().getEpochSecond();
- if (Math.abs(now - timestamp) > MAX_TIMESTAMP_DRIFT_SECONDS) {
- log.warn("Timestamp drift too large: now={}, request={}, appKey={}", now, timestamp, appKey);
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Timestamp expired");
- return;
- }
-
- // 解析 appSecret
- String appSecret;
- try {
- appSecret = secretResolver.resolveAppSecret(appKey);
- } catch (Exception e) {
- log.warn("Failed to resolve appSecret for appKey={}: {}", appKey, e.getMessage());
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid appKey");
- return;
- }
-
- if (appSecret == null || appSecret.isBlank()) {
- log.warn("No appSecret found for appKey={}", appKey);
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid appKey");
- return;
- }
-
- // 构造签名载荷并验证
- String userId = request.getHeader("X-User-Id"); // 可选
- String payload = AppRequestSignatureUtil.payload(appKey, userId, timestamp, nonce);
- if (!AppRequestSignatureUtil.matches(appSecret, payload, signature)) {
- log.warn("Signature mismatch for appKey={}", appKey);
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid signature");
- return;
- }
-
- // 签名验证通过,继续处理请求
- filterChain.doFilter(request, response);
- }
-
- /**
- * 根据 appKey 解析 appSecret。
- * 各服务需实现此接口以查询 appSecret。
- */
- public interface AppSecretResolver {
- String resolveAppSecret(String appKey);
- }
-}
diff --git a/common/src/main/java/com/xuqm/common/security/ConfigFileCrypto.java b/common/src/main/java/com/xuqm/common/security/ConfigFileCrypto.java
index ab23812..ec3d67f 100644
--- a/common/src/main/java/com/xuqm/common/security/ConfigFileCrypto.java
+++ b/common/src/main/java/com/xuqm/common/security/ConfigFileCrypto.java
@@ -109,7 +109,6 @@ public final class ConfigFileCrypto {
text(node, "harmonyBundleName"),
text(node, "baseUrl"),
text(node, "serverUrl"),
- text(node, "signingKey"),
parts[1]);
} catch (IllegalArgumentException e) {
throw e;
@@ -161,7 +160,6 @@ public final class ConfigFileCrypto {
String harmonyBundleName,
String baseUrl,
String serverUrl,
- String signingKey,
String keyId) {
public boolean matchesPackageName(String candidate) {
diff --git a/common/src/test/java/com/xuqm/common/security/ConfigFileCryptoTest.java b/common/src/test/java/com/xuqm/common/security/ConfigFileCryptoTest.java
index 70e600d..ac7d827 100644
--- a/common/src/test/java/com/xuqm/common/security/ConfigFileCryptoTest.java
+++ b/common/src/test/java/com/xuqm/common/security/ConfigFileCryptoTest.java
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
import java.security.KeyFactory;
import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
@@ -28,5 +29,8 @@ class ConfigFileCryptoTest {
assertThat(payload.configId()).isEqualTo("00000000-0000-4000-8000-000000000001");
assertThat(payload.revision()).isEqualTo(1);
assertThat(payload.packageName()).isEqualTo("com.xuqm.vector");
+ assertThat(Arrays.stream(ConfigFileCrypto.ConfigPayload.class.getRecordComponents())
+ .map(component -> component.getName()))
+ .doesNotContain("signingKey", "appSecret");
}
}
diff --git a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java
index 08084bb..676bb50 100644
--- a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java
+++ b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.xuqm.common.model.ApiResponse;
-import com.xuqm.common.security.AppRequestSignatureUtil;
import java.io.IOException;
import java.net.URI;
diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java
index c621411..243fe57 100644
--- a/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java
+++ b/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java
@@ -184,7 +184,6 @@ public class AppService {
AppEntity app = getByAppKey(appKey, tenantId);
String newSecret = generateSecret();
app.setAppSecret(newSecret);
- issueConfigFile(app, app.getConfigFileExpiresAt());
appRepository.save(app);
operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET",
"重置应用「" + app.getName() + "」的 AppSecret",
@@ -286,7 +285,6 @@ public class AppService {
payload.put("harmonyBundleName", app.getHarmonyBundleName());
}
payload.put("serverUrl", normalizeBaseUrl(sdkPlatformPublicBaseUrl));
- payload.put("signingKey", app.getAppSecret());
try {
String canonicalJson = MAPPER.writeValueAsString(payload);
app.setConfigFileContent(configFileSigningService.sign(canonicalJson));
diff --git a/tenant-service/src/test/java/com/xuqm/tenant/service/AppServiceConfigIssuanceTest.java b/tenant-service/src/test/java/com/xuqm/tenant/service/AppServiceConfigIssuanceTest.java
new file mode 100644
index 0000000..51282ca
--- /dev/null
+++ b/tenant-service/src/test/java/com/xuqm/tenant/service/AppServiceConfigIssuanceTest.java
@@ -0,0 +1,97 @@
+package com.xuqm.tenant.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xuqm.tenant.config.PrivateDeploymentProperties;
+import com.xuqm.tenant.dto.CreateAppRequest;
+import com.xuqm.tenant.entity.AppEntity;
+import com.xuqm.tenant.repository.AppRepository;
+import com.xuqm.tenant.repository.FeatureServiceRepository;
+import com.xuqm.tenant.repository.TenantRepository;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class AppServiceConfigIssuanceTest {
+
+ @Test
+ void newlyIssuedClientConfigDoesNotContainServerCredential() throws Exception {
+ AppRepository appRepository = mock(AppRepository.class);
+ OperationLogService operationLogService = mock(OperationLogService.class);
+ FeatureServiceRepository featureServiceRepository = mock(FeatureServiceRepository.class);
+ PrivateDeploymentProperties deploymentProperties = new PrivateDeploymentProperties();
+ TenantRepository tenantRepository = mock(TenantRepository.class);
+ ConfigFileSigningService signingService = mock(ConfigFileSigningService.class);
+ ArgumentCaptor