fix: stabilize auth/decrypt flow and refresh donate account counts

This commit is contained in:
rensumo
2026-02-22 16:06:22 +08:00
parent 6355c35452
commit b150641e3b
6 changed files with 64 additions and 17 deletions

View File

@@ -318,6 +318,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 只处理POST/PUT/PATCH等有body的请求方法避免GET请求读取body导致"Request has already been read"错误
String httpMethod = ctx.request().method().name();
if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.parsedHeaders() != null && ctx.parsedHeaders().contentType() != null
&& HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())
&& ctx.body() != null && ctx.body().asJsonObject() != null) {
JsonObject body = ctx.body().asJsonObject();
@@ -340,8 +341,12 @@ public class RouterHandlerFactory implements BaseHttpApi {
});
}
} else if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.body() != null) {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
&& ctx.body() != null && ctx.body().length() > 0) {
try {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
} catch (Exception e) {
LOGGER.debug("Failed to parse body as params: {}", e.getMessage());
}
}
// 解析其他参数
@@ -360,6 +365,12 @@ public class RouterHandlerFactory implements BaseHttpApi {
parameterValueList.put(k, ctx.request());
} else if (HttpServerResponse.class.getName().equals(v.getRight().getName())) {
parameterValueList.put(k, ctx.response());
} else if (JsonObject.class.getName().equals(v.getRight().getName())) {
if (ctx.body() != null && ctx.body().asJsonObject() != null) {
parameterValueList.put(k, ctx.body().asJsonObject());
} else {
parameterValueList.put(k, new JsonObject());
}
} else if (parameterValueList.get(k) == null
&& CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) {
// 绑定实体类
@@ -374,6 +385,17 @@ public class RouterHandlerFactory implements BaseHttpApi {
});
// 调用handle 获取响应对象
Object[] parameterValueArray = parameterValueList.values().toArray(new Object[0]);
// 打印调试信息,确认参数注入的情况
if (LOGGER.isDebugEnabled() && method.getName().equals("donateAccount")) {
LOGGER.debug("donateAccount parameter list:");
int i = 0;
for (Map.Entry<String, Object> entry : parameterValueList.entrySet()) {
LOGGER.debug("Param [{}]: {} = {}", i++, entry.getKey(),
entry.getValue() != null ? entry.getValue().toString() : "null");
}
}
try {
// 反射调用
Object data = ReflectionUtil.invokeWithArguments(method, instance, parameterValueArray);

View File

@@ -361,7 +361,8 @@
v-model="showDonateDialog"
title="🎁 捐赠网盘账号"
width="550px"
:close-on-click-modal="false">
:close-on-click-modal="false"
@open="loadDonateAccountCounts">
<el-alert type="info" :closable="false" show-icon style="margin-bottom: 15px;">
<template #title>
捐赠您的网盘 Cookie/Token解析时将从所有捐赠账号中随机选择使用分摊请求压力

View File

@@ -78,12 +78,17 @@ public class AuthParamCodec {
}
try {
// Step 1: URL解码
String urlDecoded = URLDecoder.decode(encryptedAuth, StandardCharsets.UTF_8);
log.debug("URL解码结果: {}", urlDecoded);
// Step 1: URL解码(兼容:有些框架已自动解码,此处避免再次把 '+' 变成空格)
String normalized = encryptedAuth;
if (normalized.contains("%")) {
normalized = URLDecoder.decode(normalized, StandardCharsets.UTF_8);
}
// 兼容 query 参数中 '+' 被还原为空格的情况
normalized = normalized.replace(' ', '+');
log.debug("认证参数规范化结果: {}", normalized);
// Step 2: Base64解码
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
byte[] base64Decoded = Base64.getDecoder().decode(normalized);
log.debug("Base64解码成功长度: {}", base64Decoded.length);
// Step 3: AES解密

View File

@@ -28,6 +28,7 @@ import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -484,12 +485,13 @@ public class ParserApi {
* 捐赠网盘账号
*/
@RouteMapping(value = "/donateAccount", method = RouteMethod.POST)
public Future<JsonObject> donateAccount(HttpServerRequest request, JsonObject body) {
public Future<JsonObject> donateAccount(RoutingContext ctx) {
JsonObject body = ctx.body().asJsonObject();
if (body == null || StringUtils.isBlank(body.getString("panType"))
|| StringUtils.isBlank(body.getString("authType"))) {
return Future.succeededFuture(JsonResult.error("panType and authType are required").toJsonObject());
}
String ip = request.remoteAddress().host();
String ip = ctx.request().remoteAddress().host();
body.put("ip", ip);
return dbService.saveDonatedAccount(body);
}

View File

@@ -366,11 +366,21 @@ public class DbServiceImpl implements DbService {
return Future.all(usernameFuture, passwordFuture, tokenFuture, failureTokenFuture)
.map(compositeFuture -> {
String username = usernameFuture.result();
String password = passwordFuture.result();
String token = tokenFuture.result();
// 如果解密后没有任何可用凭证,返回空对象,避免把密文当作明文认证参数下发给前端
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
log.warn("random donated account has no usable credential after decrypt, accountId={}", row.getLong("id"));
return JsonResult.data(new JsonObject()).toJsonObject();
}
JsonObject account = new JsonObject();
account.put("authType", row.getString("auth_type"));
account.put("username", usernameFuture.result());
account.put("password", passwordFuture.result());
account.put("token", tokenFuture.result());
account.put("username", username);
account.put("password", password);
account.put("token", token);
account.put("donatedAccountToken", failureTokenFuture.result());
return JsonResult.data(account).toJsonObject();
});
@@ -450,8 +460,10 @@ public class DbServiceImpl implements DbService {
return Future.succeededFuture(value);
}
return CryptoUtil.decrypt(value).recover(e -> {
log.warn("decrypt donated account field failed, fallback to plaintext", e);
return Future.succeededFuture(value);
// value 看起来像密文但无法解密,通常是密钥轮换/不一致导致;
// 不应回退为明文,否则会把密文误当 token/cookie 返回给调用方
log.warn("decrypt donated account field failed, fallback to null to avoid ciphertext leakage", e);
return Future.succeededFuture((String) null);
});
}

View File

@@ -30,10 +30,15 @@ public class CryptoUtil {
secretKeyFuture = ConfigUtil.readYamlConfig("secret", vertx)
.map(config -> {
String key = config.getJsonObject("encrypt").getString("key");
if (key == null || key.length() != 32) {
throw new IllegalArgumentException("Invalid AES key length in secret.yml. Key must be 32 bytes.");
if (key != null) {
key = key.trim();
}
return new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
byte[] keyBytes = key == null ? null : key.getBytes(StandardCharsets.UTF_8);
if (keyBytes == null || keyBytes.length != 32) {
int currentLen = keyBytes == null ? 0 : keyBytes.length;
throw new IllegalArgumentException("Invalid AES key length in secret.yml. Key must be 32 bytes. current=" + currentLen);
}
return new SecretKeySpec(keyBytes, "AES");
})
.onFailure(err -> logger.error("Failed to load encryption key from secret.yml", err));
} else {