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

@@ -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 {