refactor(security): remove client-embedded app secret
这个提交包含在:
父节点
f96a27facc
当前提交
228c87f5da
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 签名。
|
|
||||||
* <p>
|
|
||||||
* 请求头:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code X-App-Key} — 应用标识</li>
|
|
||||||
* <li>{@code X-Timestamp} — Unix 时间戳(秒)</li>
|
|
||||||
* <li>{@code X-Nonce} — 随机字符串(防重放)</li>
|
|
||||||
* <li>{@code X-Signature} — HMAC-SHA256 签名</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* 签名载荷格式:{@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -109,7 +109,6 @@ public final class ConfigFileCrypto {
|
|||||||
text(node, "harmonyBundleName"),
|
text(node, "harmonyBundleName"),
|
||||||
text(node, "baseUrl"),
|
text(node, "baseUrl"),
|
||||||
text(node, "serverUrl"),
|
text(node, "serverUrl"),
|
||||||
text(node, "signingKey"),
|
|
||||||
parts[1]);
|
parts[1]);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw e;
|
throw e;
|
||||||
@ -161,7 +160,6 @@ public final class ConfigFileCrypto {
|
|||||||
String harmonyBundleName,
|
String harmonyBundleName,
|
||||||
String baseUrl,
|
String baseUrl,
|
||||||
String serverUrl,
|
String serverUrl,
|
||||||
String signingKey,
|
|
||||||
String keyId) {
|
String keyId) {
|
||||||
|
|
||||||
public boolean matchesPackageName(String candidate) {
|
public boolean matchesPackageName(String candidate) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.configId()).isEqualTo("00000000-0000-4000-8000-000000000001");
|
||||||
assertThat(payload.revision()).isEqualTo(1);
|
assertThat(payload.revision()).isEqualTo(1);
|
||||||
assertThat(payload.packageName()).isEqualTo("com.xuqm.vector");
|
assertThat(payload.packageName()).isEqualTo("com.xuqm.vector");
|
||||||
|
assertThat(Arrays.stream(ConfigFileCrypto.ConfigPayload.class.getRecordComponents())
|
||||||
|
.map(component -> component.getName()))
|
||||||
|
.doesNotContain("signingKey", "appSecret");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.common.security.AppRequestSignatureUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|||||||
@ -184,7 +184,6 @@ public class AppService {
|
|||||||
AppEntity app = getByAppKey(appKey, tenantId);
|
AppEntity app = getByAppKey(appKey, tenantId);
|
||||||
String newSecret = generateSecret();
|
String newSecret = generateSecret();
|
||||||
app.setAppSecret(newSecret);
|
app.setAppSecret(newSecret);
|
||||||
issueConfigFile(app, app.getConfigFileExpiresAt());
|
|
||||||
appRepository.save(app);
|
appRepository.save(app);
|
||||||
operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET",
|
operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET",
|
||||||
"重置应用「" + app.getName() + "」的 AppSecret",
|
"重置应用「" + app.getName() + "」的 AppSecret",
|
||||||
@ -286,7 +285,6 @@ public class AppService {
|
|||||||
payload.put("harmonyBundleName", app.getHarmonyBundleName());
|
payload.put("harmonyBundleName", app.getHarmonyBundleName());
|
||||||
}
|
}
|
||||||
payload.put("serverUrl", normalizeBaseUrl(sdkPlatformPublicBaseUrl));
|
payload.put("serverUrl", normalizeBaseUrl(sdkPlatformPublicBaseUrl));
|
||||||
payload.put("signingKey", app.getAppSecret());
|
|
||||||
try {
|
try {
|
||||||
String canonicalJson = MAPPER.writeValueAsString(payload);
|
String canonicalJson = MAPPER.writeValueAsString(payload);
|
||||||
app.setConfigFileContent(configFileSigningService.sign(canonicalJson));
|
app.setConfigFileContent(configFileSigningService.sign(canonicalJson));
|
||||||
|
|||||||
@ -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<String> canonicalPayload = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
|
when(appRepository.save(any(AppEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(signingService.sign(canonicalPayload.capture())).thenReturn("signed-config");
|
||||||
|
|
||||||
|
AppService service = new AppService(
|
||||||
|
appRepository,
|
||||||
|
operationLogService,
|
||||||
|
featureServiceRepository,
|
||||||
|
deploymentProperties,
|
||||||
|
tenantRepository,
|
||||||
|
signingService);
|
||||||
|
ReflectionTestUtils.setField(service, "sdkPlatformPublicBaseUrl", "https://sdk.example.com/");
|
||||||
|
|
||||||
|
AppEntity created = service.create("tenant-1", new CreateAppRequest(
|
||||||
|
"com.example.android",
|
||||||
|
"com.example.ios",
|
||||||
|
"com.example.harmony",
|
||||||
|
"Example",
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
JsonNode payload = new ObjectMapper().readTree(canonicalPayload.getValue());
|
||||||
|
assertThat(created.getConfigFileContent()).isEqualTo("signed-config");
|
||||||
|
assertThat(payload.path("appKey").asText()).isEqualTo(created.getAppKey());
|
||||||
|
assertThat(payload.path("serverUrl").asText()).isEqualTo("https://sdk.example.com/");
|
||||||
|
assertThat(payload.has("signingKey")).isFalse();
|
||||||
|
assertThat(payload.has("appSecret")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resettingServerAppSecretDoesNotInvalidateIndependentClientConfig() {
|
||||||
|
AppRepository appRepository = mock(AppRepository.class);
|
||||||
|
OperationLogService operationLogService = mock(OperationLogService.class);
|
||||||
|
FeatureServiceRepository featureServiceRepository = mock(FeatureServiceRepository.class);
|
||||||
|
ConfigFileSigningService signingService = mock(ConfigFileSigningService.class);
|
||||||
|
AppEntity app = new AppEntity();
|
||||||
|
app.setId("app-1");
|
||||||
|
app.setTenantId("tenant-1");
|
||||||
|
app.setAppKey("ak_test");
|
||||||
|
app.setAppSecret("old-secret");
|
||||||
|
app.setName("Example");
|
||||||
|
app.setPackageName("com.example");
|
||||||
|
app.setConfigFileContent("unchanged-signed-config");
|
||||||
|
app.setConfigFileId("config-1");
|
||||||
|
app.setConfigFileRevision(7);
|
||||||
|
|
||||||
|
when(appRepository.findByAppKey("ak_test")).thenReturn(java.util.Optional.of(app));
|
||||||
|
when(appRepository.save(any(AppEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
AppService service = new AppService(
|
||||||
|
appRepository,
|
||||||
|
operationLogService,
|
||||||
|
featureServiceRepository,
|
||||||
|
new PrivateDeploymentProperties(),
|
||||||
|
mock(TenantRepository.class),
|
||||||
|
signingService);
|
||||||
|
|
||||||
|
String newSecret = service.resetSecret("ak_test", "tenant-1");
|
||||||
|
|
||||||
|
assertThat(newSecret).isNotBlank().isNotEqualTo("old-secret");
|
||||||
|
assertThat(app.getConfigFileContent()).isEqualTo("unchanged-signed-config");
|
||||||
|
assertThat(app.getConfigFileRevision()).isEqualTo(7);
|
||||||
|
verify(signingService, never()).sign(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package com.xuqm.update.config;
|
package com.xuqm.update.config;
|
||||||
|
|
||||||
import com.xuqm.common.security.ApiKeyAuthFilter;
|
import com.xuqm.common.security.ApiKeyAuthFilter;
|
||||||
import com.xuqm.common.security.AppSignatureAuthFilter;
|
|
||||||
import com.xuqm.common.security.JwtAuthFilter;
|
import com.xuqm.common.security.JwtAuthFilter;
|
||||||
import com.xuqm.common.security.JwtUtil;
|
import com.xuqm.common.security.JwtUtil;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@ -26,13 +25,10 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
private final TenantApiKeyValidator apiKeyValidator;
|
private final TenantApiKeyValidator apiKeyValidator;
|
||||||
private final TenantAppSecretResolver appSecretResolver;
|
|
||||||
|
|
||||||
public SecurityConfig(JwtUtil jwtUtil, TenantApiKeyValidator apiKeyValidator,
|
public SecurityConfig(JwtUtil jwtUtil, TenantApiKeyValidator apiKeyValidator) {
|
||||||
TenantAppSecretResolver appSecretResolver) {
|
|
||||||
this.jwtUtil = jwtUtil;
|
this.jwtUtil = jwtUtil;
|
||||||
this.apiKeyValidator = apiKeyValidator;
|
this.apiKeyValidator = apiKeyValidator;
|
||||||
this.appSecretResolver = appSecretResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -67,8 +63,6 @@ public class SecurityConfig {
|
|||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
.addFilterBefore(new RnBearerAuthFilter(apiKeyValidator),
|
.addFilterBefore(new RnBearerAuthFilter(apiKeyValidator),
|
||||||
UsernamePasswordAuthenticationFilter.class)
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
// HMAC 签名验证(SDK 请求)
|
|
||||||
.addFilterBefore(new AppSignatureAuthFilter(appSecretResolver), UsernamePasswordAuthenticationFilter.class)
|
|
||||||
// JWT 认证
|
// JWT 认证
|
||||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
|
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
|
||||||
.httpBasic(AbstractHttpConfigurer::disable)
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
package com.xuqm.update.config;
|
|
||||||
|
|
||||||
import com.xuqm.common.security.AppSignatureAuthFilter;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过 tenant-service 内部 API 解析 appSecret。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class TenantAppSecretResolver implements AppSignatureAuthFilter.AppSecretResolver {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TenantAppSecretResolver.class);
|
|
||||||
|
|
||||||
@Value("${sdk.tenant-service-url:http://xuqm-tenant-service:9001}")
|
|
||||||
private String tenantServiceUrl;
|
|
||||||
|
|
||||||
@Value("${sdk.internal-token:xuqm-internal-token}")
|
|
||||||
private String internalToken;
|
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
/** 缓存:appKey → appSecret */
|
|
||||||
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String resolveAppSecret(String appKey) {
|
|
||||||
// 先查缓存
|
|
||||||
String cached = cache.get(appKey);
|
|
||||||
if (cached != null) return cached;
|
|
||||||
|
|
||||||
try {
|
|
||||||
String url = tenantServiceUrl + "/api/internal/sdk/apps/" + appKey + "/secret";
|
|
||||||
var headers = new org.springframework.http.HttpHeaders();
|
|
||||||
headers.set("X-Internal-Token", internalToken);
|
|
||||||
var entity = new org.springframework.http.HttpEntity<>(headers);
|
|
||||||
|
|
||||||
var response = restTemplate.exchange(
|
|
||||||
url,
|
|
||||||
org.springframework.http.HttpMethod.GET,
|
|
||||||
entity,
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
||||||
JsonNode root = objectMapper.readTree(response.getBody());
|
|
||||||
JsonNode data = root.path("data");
|
|
||||||
String appSecret = data.path("appSecret").asText(null);
|
|
||||||
if (appSecret != null) {
|
|
||||||
cache.put(appKey, appSecret);
|
|
||||||
}
|
|
||||||
return appSecret;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to resolve appSecret for appKey={}: {}", appKey, e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户