Compare commits

...

4 Commits

Author SHA1 Message Date
q b70e30796f ci: package fuller jre for native builds 2026-07-19 09:57:31 +08:00
q d3ef521773 fix: support donated auth for 123 directory downloads 2026-07-19 09:39:55 +08:00
q 2969c92d3d build jre 2026-07-19 02:06:33 +08:00
q 2fde421169 chore: bump version to 0.3.9 2026-07-19 01:11:57 +08:00
8 changed files with 271 additions and 253 deletions
+13 -26
View File
@@ -158,37 +158,19 @@ jobs:
path: web-service/target/package path: web-service/target/package
# ============================================================ # ============================================================
# jdeps 分析 → 确定所需 JDK 模块 # 选择 JDK 模块
# ============================================================ # ============================================================
- name: 分析所需 JDK 模块jdeps - name: 选择完整 JDK 模块集合
run: | run: |
MAIN_JAR="web-service/target/package/netdisk-fast-download.jar" # 123 网盘/自定义 JS 解析器会通过 javax.script、Nashorn 和 SPI/反射加载模块,
LIB_DIR="web-service/target/package/lib" # jdeps 无法稳定识别这些运行时依赖。这里优先保证原生包可运行,使用完整
# JDK 模块集合生成运行时,避免 Windows 精简 JRE 缺 java.scripting/jdk.dynalink 等模块。
CP="" printf 'JDK_MODULES=ALL-MODULE-PATH\n' >> $GITHUB_ENV
for jar in "$LIB_DIR"/*.jar; do
CP="$CP${CP:+:}$jar"
done
RAW_MODULES=$(jdeps --print-module-deps --ignore-missing-deps --multi-release 17 \
--class-path "$CP" "$MAIN_JAR" 2>/dev/null | head -n 1 | tr -d '\r\n' || true)
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
# ⚠️ 回退列表:若项目新增了需要 java.* / jdk.* 模块的依赖,需同步更新此处
RAW_MODULES="java.base,java.logging,java.sql,java.naming,java.management,java.xml,jdk.unsupported,java.net.http,java.instrument,java.security.jgss,java.security.sasl,java.desktop,jdk.crypto.ec"
echo "jdeps 分析失败,使用回退模块列表"
else
# 补上 jdeps 无法检测的反射/SPI依赖
RAW_MODULES="$RAW_MODULES,java.desktop,jdk.crypto.ec"
fi
echo "detected modules: $RAW_MODULES"
printf 'JDK_MODULES=%s\n' "$RAW_MODULES" >> $GITHUB_ENV
# ============================================================ # ============================================================
# jlink 生成精简 JRE # jlink 生成完整模块 JRE
# ============================================================ # ============================================================
- name: 生成精简 JREjlink - name: 生成完整模块 JREjlink
run: | run: |
jlink \ jlink \
--module-path "$JAVA_HOME/jmods" \ --module-path "$JAVA_HOME/jmods" \
@@ -215,6 +197,11 @@ jobs:
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)" ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
fi fi
echo "=== 关键 JDK 模块校验 ==="
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^java.scripting@'
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^jdk.dynalink@'
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^jdk.unsupported@'
# ============================================================ # ============================================================
# 组装包目录 # 组装包目录
# ============================================================ # ============================================================
@@ -641,7 +641,14 @@ public class Ye2Tool extends PanBase {
return; return;
} }
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey()); String rawShareKey = paramJson.getString("shareKey");
if (StringUtils.isBlank(rawShareKey)) {
rawShareKey = paramJson.getString("ShareKey");
}
if (StringUtils.isBlank(rawShareKey)) {
rawShareKey = shareLinkInfo.getShareKey();
}
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(rawShareKey);
if (StringUtils.isNotEmpty(normalizedShareKey)) { if (StringUtils.isNotEmpty(normalizedShareKey)) {
JsonObject v2Body = new JsonObject() JsonObject v2Body = new JsonObject()
.put("ShareKey", normalizedShareKey) .put("ShareKey", normalizedShareKey)
+1 -1
View File
@@ -17,7 +17,7 @@
</modules> </modules>
<properties> <properties>
<revision>0.3.8</revision> <revision>0.4.1</revision>
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "nfd-web", "name": "nfd-web",
"version": "0.3.8", "version": "0.4.1",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -0,0 +1,203 @@
package cn.qaiu.lz.common.util;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.web.model.AuthParam;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
@Slf4j
public class ParserAuthUtil {
public static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
public static final String TEMP_AUTH_ADDED = "__TEMP_AUTH_ADDED";
public static final String DONATED_ACCOUNT_TOKEN = "__AUTO_DONATED_ACCOUNT_TOKEN";
private ParserAuthUtil() {
}
public static JsonObject buildOtherParam(HttpServerRequest request, String auth, String requestOrigin) {
return buildOtherParam(request, auth, requestOrigin, false);
}
public static JsonObject buildOtherParam(HttpServerRequest request, String auth, String requestOrigin,
boolean skipClientLinks) {
JsonObject otherParam = JsonObject.of(
"UA", request.headers().get("user-agent"),
"_requestOrigin", requestOrigin
);
if (skipClientLinks) {
otherParam.put(SKIP_CLIENT_LINKS, true);
}
if (StringUtils.isNotBlank(auth)) {
AuthParam authParam = AuthParamCodec.decode(auth);
if (authParam != null && authParam.hasValidAuth()) {
otherParam.put("authType", authParam.getAuthType());
otherParam.put("authToken", authParam.getPrimaryCredential());
otherParam.put("authPassword", authParam.getPassword());
otherParam.put("authInfo1", authParam.getExt1());
otherParam.put("authInfo2", authParam.getExt2());
otherParam.put("authInfo3", authParam.getExt3());
otherParam.put("authInfo4", authParam.getExt4());
otherParam.put("authInfo5", authParam.getExt5());
if (StringUtils.isNotBlank(authParam.getDonatedAccountToken())) {
otherParam.put("donatedAccountToken", authParam.getDonatedAccountToken());
}
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
}
}
return otherParam;
}
public static Future<Void> applyAuthParamsAndDonatedFallback(ParserCreate parserCreate, JsonObject otherParam,
DbService dbService) {
JsonObject params = otherParam == null ? new JsonObject() : otherParam;
parserCreate.getShareLinkInfo().getOtherParam().putAll(params.getMap());
if (params.containsKey("authType") || params.containsKey("authToken")) {
log.debug("从otherParam中检测到临时认证参数");
URLParamUtil.addTempAuthParam(parserCreate,
params.getString("authType"),
params.getString("authToken"),
params.getString("authPassword"),
params.getString("authInfo1"),
params.getString("authInfo2"),
params.getString("authInfo3"),
params.getString("authInfo4"),
params.getString("authInfo5"));
}
return applyDonatedAccountFallback(parserCreate, dbService);
}
public static void recordDonatedAccountFailureIfNeeded(DbService dbService, JsonObject otherParam,
Throwable cause) {
if (!isLikelyAuthFailure(cause) || otherParam == null) {
return;
}
String donatedAccountToken = otherParam.getString("donatedAccountToken");
if (StringUtils.isBlank(donatedAccountToken)) {
return;
}
dbService.recordDonatedAccountFailureByToken(donatedAccountToken)
.onFailure(e -> log.warn("记录捐赠账号失败次数失败", e));
}
public static void recordAutoDonatedFailureIfNeeded(DbService dbService, ShareLinkInfo shareLinkInfo,
Throwable cause) {
if (shareLinkInfo == null || !isLikelyAuthFailure(cause)) {
return;
}
Object tokenObj = shareLinkInfo.getOtherParam().get(DONATED_ACCOUNT_TOKEN);
if (!(tokenObj instanceof String) || StringUtils.isBlank((String) tokenObj)) {
return;
}
dbService.recordDonatedAccountFailureByToken((String) tokenObj)
.onFailure(e -> log.warn("记录自动捐赠账号失败次数失败", e));
}
private static Future<Void> applyDonatedAccountFallback(ParserCreate parserCreate, DbService dbService) {
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
Map<String, Object> otherParam = shareLinkInfo.getOtherParam();
if (Boolean.TRUE.equals(otherParam.get(TEMP_AUTH_ADDED))) {
return Future.succeededFuture();
}
String type = shareLinkInfo.getType();
if (StringUtils.isBlank(type) || hasUsableStaticAuthConfig(type)) {
return Future.succeededFuture();
}
return dbService.getRandomDonatedAccount(type.toUpperCase())
.compose(res -> {
if (!Integer.valueOf(200).equals(res.getInteger("code"))) {
return Future.succeededFuture();
}
JsonObject data = res.getJsonObject("data");
if (data == null || data.isEmpty()) {
return Future.succeededFuture();
}
String username = data.getString("username");
String password = data.getString("password");
String token = data.getString("token");
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
return Future.succeededFuture();
}
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();
if (StringUtils.isNotBlank(username)) {
tempAuth.set("username", username);
}
if (StringUtils.isNotBlank(password)) {
tempAuth.set("password", password);
}
if (StringUtils.isNotBlank(token)) {
tempAuth.set("token", token);
}
otherParam.put(ConfigConstant.AUTHS, tempAuth);
otherParam.put(TEMP_AUTH_ADDED, true);
String donatedAccountToken = data.getString("donatedAccountToken");
if (StringUtils.isNotBlank(donatedAccountToken)) {
otherParam.put(DONATED_ACCOUNT_TOKEN, donatedAccountToken);
}
log.debug("已自动应用捐赠账号: type={}", type);
return Future.<Void>succeededFuture();
})
.recover(err -> {
log.warn("自动获取捐赠账号失败: type={}", type, err);
return Future.succeededFuture();
});
}
private static boolean hasUsableStaticAuthConfig(String type) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
if (!localMap.containsKey(ConfigConstant.AUTHS)) {
return false;
}
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
JsonObject cfg = auths.getJsonObject(type);
if (cfg == null) {
return false;
}
for (String key : cfg.fieldNames()) {
Object value = cfg.getValue(key);
if (value != null && StringUtils.isNotBlank(value.toString())) {
return true;
}
}
return false;
}
private static boolean isLikelyAuthFailure(Throwable cause) {
if (cause == null) {
return false;
}
String msg = cause.getMessage();
if (StringUtils.isBlank(msg)) {
return false;
}
String lower = msg.toLowerCase();
return lower.contains("auth")
|| lower.contains("token")
|| lower.contains("cookie")
|| lower.contains("password")
|| lower.contains("credential")
|| lower.contains("401")
|| lower.contains("403")
|| lower.contains("unauthorized")
|| lower.contains("forbidden")
|| lower.contains("expired")
|| lower.contains("登录")
|| lower.contains("认证");
}
}
@@ -5,6 +5,7 @@ import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheManager; import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.util.AuthParamCodec; import cn.qaiu.lz.common.util.AuthParamCodec;
import cn.qaiu.lz.common.util.ParserAuthUtil;
import cn.qaiu.lz.common.util.URLParamUtil; import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.AuthParam; import cn.qaiu.lz.web.model.AuthParam;
import cn.qaiu.lz.web.model.CacheLinkInfo; import cn.qaiu.lz.web.model.CacheLinkInfo;
@@ -159,7 +160,8 @@ public class ParserApi {
} }
@RouteMapping("/getFileList") @RouteMapping("/getFileList")
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) { public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid,
String auth) {
String url = URLParamUtil.parserParams(request); String url = URLParamUtil.parserParams(request);
ParserCreate parserCreate; ParserCreate parserCreate;
try { try {
@@ -168,6 +170,7 @@ public class ParserApi {
return Future.failedFuture(e); return Future.failedFuture(e);
} }
String linkPrefix = getLinkPrefix(request); String linkPrefix = getLinkPrefix(request);
JsonObject otherParam = ParserAuthUtil.buildOtherParam(request, auth, linkPrefix);
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix); parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix); parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
if (StringUtils.isNotBlank(dirId)) { if (StringUtils.isNotBlank(dirId)) {
@@ -176,13 +179,22 @@ public class ParserApi {
if (StringUtils.isNotBlank(uuid)) { if (StringUtils.isNotBlank(uuid)) {
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid); parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
} }
IPanTool tool = parserCreate.createTool(); return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
return IPanTool.closeAfter(tool, tool::parseFileList); .compose(v -> {
URLParamUtil.addParam(parserCreate);
IPanTool tool = parserCreate.createTool();
return IPanTool.closeAfter(tool, tool::parseFileList)
.onFailure(t -> {
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService,
parserCreate.getShareLinkInfo(), t);
});
});
} }
// 目录解析下载文件 // 目录解析下载文件
// @RouteMapping("/getFileDownUrl/:type/:param") // @RouteMapping("/getFileDownUrl/:type/:param")
public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) { public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param, String auth) {
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
.setShareLinkInfoPwd("-"); .setShareLinkInfoPwd("-");
@@ -198,17 +210,28 @@ public class ParserApi {
// domainName // domainName
String linkPrefix = getLinkPrefix(request); String linkPrefix = getLinkPrefix(request);
JsonObject otherParam = ParserAuthUtil.buildOtherParam(request, auth, linkPrefix, true);
shareLinkInfo.getOtherParam().put("domainName", linkPrefix); shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix); shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
IPanTool tool = parserCreate.createTool(); return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
return IPanTool.closeAfter(tool, tool::parseById); .compose(v -> {
URLParamUtil.addParam(parserCreate);
IPanTool tool = parserCreate.createTool();
return IPanTool.closeAfter(tool, tool::parseById)
.onFailure(t -> {
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService,
parserCreate.getShareLinkInfo(), t);
});
});
} }
@RouteMapping("/redirectUrl/:type/:param") @RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) { public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param,
String auth) {
Promise<Void> promise = Promise.promise(); Promise<Void> promise = Promise.promise();
getFileDownUrl(request, type, param) getFileDownUrl(request, type, param, auth)
.onSuccess(res -> { .onSuccess(res -> {
ResponseUtil.redirect(response, res, promise); ResponseUtil.redirect(response, res, promise);
}) })
@@ -283,11 +306,12 @@ public class ParserApi {
@RouteMapping("/viewUrl/:type/:param") @RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) { public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param,
String auth) {
Promise<Void> promise = Promise.promise(); Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL"); String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(request, type, param) getFileDownUrl(request, type, param, auth)
.onSuccess(res -> { .onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8); String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url, promise); ResponseUtil.redirect(response, url, promise);
@@ -1,8 +1,7 @@
package cn.qaiu.lz.web.controller; package cn.qaiu.lz.web.controller;
import cn.qaiu.lz.common.util.AuthParamCodec; import cn.qaiu.lz.common.util.ParserAuthUtil;
import cn.qaiu.lz.common.util.URLParamUtil; import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.AuthParam;
import cn.qaiu.lz.web.model.CacheLinkInfo; import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.service.CacheService; import cn.qaiu.lz.web.service.CacheService;
import cn.qaiu.lz.web.service.DbService; import cn.qaiu.lz.web.service.DbService;
@@ -29,7 +28,7 @@ import lombok.extern.slf4j.Slf4j;
@RouteHandler("/") @RouteHandler("/")
public class ServerApi { public class ServerApi {
private static final String SKIP_CLIENT_LINKS = "_skipClientLinks"; private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS;
private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class); private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class);
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@@ -47,7 +46,7 @@ public class ServerApi {
addCacheHeaders(response, res), addCacheHeaders(response, res),
res.getDirectLink(), promise)) res.getDirectLink(), promise))
.onFailure(t -> { .onFailure(t -> {
recordDonatedAccountFailureIfNeeded(otherParam, t); ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
promise.tryFail(t); promise.tryFail(t);
}); });
return promise.future(); return promise.future();
@@ -58,14 +57,14 @@ public class ServerApi {
String url = URLParamUtil.parserParams(request); String url = URLParamUtil.parserParams(request);
JsonObject otherParam = buildOtherParam(request, auth); JsonObject otherParam = buildOtherParam(request, auth);
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam) return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
.onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t)); .onFailure(t -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t));
} }
public Future<CacheLinkInfo> parseJsonForRedirect(HttpServerRequest request, String pwd, String auth) { public Future<CacheLinkInfo> parseJsonForRedirect(HttpServerRequest request, String pwd, String auth) {
String url = URLParamUtil.parserParams(request); String url = URLParamUtil.parserParams(request);
JsonObject otherParam = buildOtherParam(request, auth, true); JsonObject otherParam = buildOtherParam(request, auth, true);
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam) return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
.onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t)); .onFailure(t -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t));
} }
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET) @RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
@@ -146,66 +145,6 @@ public class ServerApi {
} }
private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) { private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) {
JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent"), "_requestOrigin", resolveOrigin(request)); return ParserAuthUtil.buildOtherParam(request, auth, resolveOrigin(request), skipClientLinks);
if (skipClientLinks) {
otherParam.put(SKIP_CLIENT_LINKS, true);
}
// 解码认证参数
if (auth != null && !auth.isEmpty()) {
AuthParam authParam = AuthParamCodec.decode(auth);
if (authParam != null && authParam.hasValidAuth()) {
// 将认证参数放入 otherParam
otherParam.put("authType", authParam.getAuthType());
otherParam.put("authToken", authParam.getPrimaryCredential());
otherParam.put("authPassword", authParam.getPassword());
otherParam.put("authInfo1", authParam.getExt1());
otherParam.put("authInfo2", authParam.getExt2());
otherParam.put("authInfo3", authParam.getExt3());
otherParam.put("authInfo4", authParam.getExt4());
otherParam.put("authInfo5", authParam.getExt5());
if (authParam.getDonatedAccountToken() != null && !authParam.getDonatedAccountToken().isBlank()) {
otherParam.put("donatedAccountToken", authParam.getDonatedAccountToken());
}
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
}
}
return otherParam;
}
private void recordDonatedAccountFailureIfNeeded(JsonObject otherParam, Throwable cause) {
if (!isLikelyAuthFailure(cause)) {
return;
}
String donatedAccountToken = otherParam.getString("donatedAccountToken");
if (donatedAccountToken == null || donatedAccountToken.isBlank()) {
return;
}
dbService.recordDonatedAccountFailureByToken(donatedAccountToken)
.onFailure(e -> log.warn("记录捐赠账号失败次数失败", e));
}
private boolean isLikelyAuthFailure(Throwable cause) {
if (cause == null) {
return false;
}
String msg = cause.getMessage();
if (msg == null || msg.isBlank()) {
return false;
}
String lower = msg.toLowerCase();
return lower.contains("auth")
|| lower.contains("token")
|| lower.contains("cookie")
|| lower.contains("password")
|| lower.contains("credential")
|| lower.contains("401")
|| lower.contains("403")
|| lower.contains("unauthorized")
|| lower.contains("forbidden")
|| lower.contains("expired")
|| lower.contains("登录")
|| lower.contains("认证");
} }
} }
@@ -5,6 +5,7 @@ import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheConfigLoader; import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.cache.CacheManager; import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.cache.CacheTotalField; import cn.qaiu.lz.common.cache.CacheTotalField;
import cn.qaiu.lz.common.util.ParserAuthUtil;
import cn.qaiu.lz.common.util.URLParamUtil; import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.CacheLinkInfo; import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.service.CacheService; import cn.qaiu.lz.web.service.CacheService;
@@ -15,15 +16,10 @@ import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
import cn.qaiu.parser.clientlink.ClientLinkType; import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.vx.core.annotaions.Service; import cn.qaiu.vx.core.annotaions.Service;
import cn.qaiu.vx.core.util.AsyncServiceUtil; import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date; import java.util.Date;
@@ -34,9 +30,7 @@ import java.util.Map;
@Slf4j @Slf4j
public class CacheServiceImpl implements CacheService { public class CacheServiceImpl implements CacheService {
private static final String SKIP_CLIENT_LINKS = "_skipClientLinks"; private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS;
private static final String TEMP_AUTH_ADDED = "__TEMP_AUTH_ADDED";
private static final String DONATED_ACCOUNT_TOKEN = "__AUTO_DONATED_ACCOUNT_TOKEN";
private final CacheManager cacheManager = new CacheManager(); private final CacheManager cacheManager = new CacheManager();
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@@ -82,7 +76,7 @@ public class CacheServiceImpl implements CacheService {
return; return;
} }
IPanTool.closeAfter(tool, tool::parse).onFailure(err -> { IPanTool.closeAfter(tool, tool::parse).onFailure(err -> {
recordAutoDonatedFailureIfNeeded(shareLinkInfo, err); ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService, shareLinkInfo, err);
}).onSuccess(redirectUrl -> { }).onSuccess(redirectUrl -> {
// 使用 effectiveCacheDuration // 使用 effectiveCacheDuration
long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L; long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L;
@@ -153,124 +147,6 @@ public class CacheServiceImpl implements CacheService {
return DateFormatUtils.format(new Date(ts), "yyyy-MM-dd HH:mm:ss"); return DateFormatUtils.format(new Date(ts), "yyyy-MM-dd HH:mm:ss");
} }
/**
* 当本次请求没有携带临时认证参数(auth=xxx),且该网盘类型在 app-dev.yml 中也没有配置可用的
* 静态账号密码/token 时,自动从捐赠账号池中随机挑选一个可用账号使用,
* 这样捐赠者捐赠账号后,普通请求(不带 auth 参数)也能直接受益,无需前端额外拼接参数。
*/
private Future<Void> applyDonatedAccountFallback(ParserCreate parserCreate) {
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
Map<String, Object> otherParam = shareLinkInfo.getOtherParam();
if (Boolean.TRUE.equals(otherParam.get(TEMP_AUTH_ADDED))) {
// 请求已经携带了个人配置/捐赠账号的临时认证参数,无需再自动回退
return Future.succeededFuture();
}
String type = shareLinkInfo.getType();
if (StringUtils.isBlank(type) || hasUsableStaticAuthConfig(type)) {
return Future.succeededFuture();
}
return dbService.getRandomDonatedAccount(type.toUpperCase())
.compose(res -> {
if (!Integer.valueOf(200).equals(res.getInteger("code"))) {
return Future.succeededFuture();
}
JsonObject data = res.getJsonObject("data");
if (data == null || data.isEmpty()) {
return Future.succeededFuture();
}
String username = data.getString("username");
String password = data.getString("password");
String token = data.getString("token");
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
return Future.succeededFuture();
}
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();
if (StringUtils.isNotBlank(username)) {
tempAuth.set("username", username);
}
if (StringUtils.isNotBlank(password)) {
tempAuth.set("password", password);
}
if (StringUtils.isNotBlank(token)) {
tempAuth.set("token", token);
}
otherParam.put(ConfigConstant.AUTHS, tempAuth);
otherParam.put(TEMP_AUTH_ADDED, true);
String donatedAccountToken = data.getString("donatedAccountToken");
if (StringUtils.isNotBlank(donatedAccountToken)) {
otherParam.put(DONATED_ACCOUNT_TOKEN, donatedAccountToken);
}
log.debug("已自动应用捐赠账号: type={}", type);
return Future.<Void>succeededFuture();
})
.recover(err -> {
log.warn("自动获取捐赠账号失败: type={}", type, err);
return Future.succeededFuture();
});
}
/**
* 检查 app-dev.yml 中该网盘类型是否已配置了可用的静态认证信息(非空的 username/password/token/authorization 等)。
* 如果已配置,则不需要再自动回退到捐赠账号池。
*/
private boolean hasUsableStaticAuthConfig(String type) {
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(ConfigConstant.LOCAL);
if (!localMap.containsKey(ConfigConstant.AUTHS)) {
return false;
}
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
JsonObject cfg = auths.getJsonObject(type);
if (cfg == null) {
return false;
}
for (String key : cfg.fieldNames()) {
Object value = cfg.getValue(key);
if (value != null && StringUtils.isNotBlank(value.toString())) {
return true;
}
}
return false;
}
/**
* 自动回退使用的捐赠账号解析失败时,记录一次失败次数(达到阈值后台账号会被自动禁用)。
*/
private void recordAutoDonatedFailureIfNeeded(ShareLinkInfo shareLinkInfo, Throwable cause) {
Object tokenObj = shareLinkInfo.getOtherParam().get(DONATED_ACCOUNT_TOKEN);
if (!(tokenObj instanceof String) || StringUtils.isBlank((String) tokenObj)) {
return;
}
if (!isLikelyAuthFailure(cause)) {
return;
}
dbService.recordDonatedAccountFailureByToken((String) tokenObj)
.onFailure(e -> log.warn("记录自动捐赠账号失败次数失败", e));
}
private boolean isLikelyAuthFailure(Throwable cause) {
if (cause == null) {
return false;
}
String msg = cause.getMessage();
if (msg == null || msg.isBlank()) {
return false;
}
String lower = msg.toLowerCase();
return lower.contains("auth")
|| lower.contains("token")
|| lower.contains("cookie")
|| lower.contains("password")
|| lower.contains("credential")
|| lower.contains("401")
|| lower.contains("403")
|| lower.contains("unauthorized")
|| lower.contains("forbidden")
|| lower.contains("expired")
|| lower.contains("登录")
|| lower.contains("认证");
}
private boolean shouldGenerateClientLinks(ShareLinkInfo shareLinkInfo) { private boolean shouldGenerateClientLinks(ShareLinkInfo shareLinkInfo) {
if (shareLinkInfo == null || shareLinkInfo.getOtherParam() == null) { if (shareLinkInfo == null || shareLinkInfo.getOtherParam() == null) {
return true; return true;
@@ -408,10 +284,8 @@ public class CacheServiceImpl implements CacheService {
} catch (Exception e) { } catch (Exception e) {
return Future.failedFuture(e); return Future.failedFuture(e);
} }
parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
ParserCreate finalParserCreate = parserCreate; ParserCreate finalParserCreate = parserCreate;
return applyDonatedAccountFallback(finalParserCreate) return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService)
.compose(v -> getAndSaveCachedShareLink(finalParserCreate)); .compose(v -> getAndSaveCachedShareLink(finalParserCreate));
} }
@@ -423,24 +297,8 @@ public class CacheServiceImpl implements CacheService {
} catch (Exception e) { } catch (Exception e) {
return Future.failedFuture(e); return Future.failedFuture(e);
} }
parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
// 检查是否有临时认证参数
if (otherParam.containsKey("authType") || otherParam.containsKey("authToken")) {
log.debug("从otherParam中检测到临时认证参数");
URLParamUtil.addTempAuthParam(parserCreate,
otherParam.getString("authType"),
otherParam.getString("authToken"),
otherParam.getString("authPassword"),
otherParam.getString("authInfo1"),
otherParam.getString("authInfo2"),
otherParam.getString("authInfo3"),
otherParam.getString("authInfo4"),
otherParam.getString("authInfo5"));
}
ParserCreate finalParserCreate = parserCreate; ParserCreate finalParserCreate = parserCreate;
return applyDonatedAccountFallback(finalParserCreate) return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService)
.compose(v -> getAndSaveCachedShareLink(finalParserCreate)); .compose(v -> getAndSaveCachedShareLink(finalParserCreate));
} }
} }