From 675d930f868520a0dd4ba3fc74b297db7dbb4702 Mon Sep 17 00:00:00 2001 From: q Date: Sat, 18 Jul 2026 23:21:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20123=E7=BD=91=E7=9B=98=E6=8D=90=E8=B5=A0?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=97=A0=E9=9C=80=E4=B8=B4=E6=97=B6auth?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=8D=B3=E5=8F=AF=E8=87=AA=E5=8A=A8=E7=94=9F?= =?UTF-8?q?=E6=95=88=EF=BC=8C=E4=BF=AE=E5=A4=8Dtoken=E5=89=8D=E7=BC=80?= =?UTF-8?q?=E4=B8=8E=E8=84=8F=E6=95=B0=E6=8D=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端: getCurrentPanType() 补全123网盘域名匹配正则(share.123pan.cn等子域名) - 前端: 临时认证弹窗补充123云盘(YE)选项及提示文案 - 前端: submitDonateAccount() 按当前authType只提交相关字段,避免切换认证方式后 残留的用户名/密码脏数据被一并提交 - 后端: URLParamUtil.addParam() 不再无条件用静态yml配置覆盖已生效的临时认证/ 自动捐赠账号回退(增加 __TEMP_AUTH_ADDED 判断) - 后端: CacheServiceImpl 新增 applyDonatedAccountFallback(),请求未带auth参数 且无静态配置时自动从捐赠账号池随机取一个可用账号使用,并在解析失败时记录/ 自动禁用失败账号 - 后端: DbServiceImpl.getRandomDonatedAccount() 对非password类型账号强制丢弃 历史遗留的username/password字段,避免被误当账号密码登录 - 解析器: Ye2Tool 清理token/authorization前缀中的"Bearer ",避免拼接成 'Bearer Bearer xxx' 导致123接口返回 tokenstring should not contain bearer 已通过本地实测验证: 捐赠123账号后,不带auth参数直接请求 /json/parser?url=https://1815268665.share.123pan.cn/123pan/iaKtVv-r4aCd 可正常返回200及直链 --- .../java/cn/qaiu/parser/impl/Ye2Tool.java | 20 ++- web-front/src/views/Home.vue | 23 ++- .../cn/qaiu/lz/common/util/URLParamUtil.java | 34 +++-- .../lz/web/service/impl/CacheServiceImpl.java | 143 +++++++++++++++++- .../lz/web/service/impl/DbServiceImpl.java | 21 ++- 5 files changed, 211 insertions(+), 30 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/impl/Ye2Tool.java b/parser/src/main/java/cn/qaiu/parser/impl/Ye2Tool.java index e63d577..55959fb 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/Ye2Tool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/Ye2Tool.java @@ -99,10 +99,26 @@ public class Ye2Tool extends PanBase { return null; } String token = auths.get("token"); - if (StringUtils.isNotEmpty(token)) { + if (StringUtils.isEmpty(token)) { + token = auths.get("authorization"); + } + return stripBearerPrefix(token); + } + + /** + * 用户/捐赠账号提供的 token 经常是从浏览器开发者工具里连同 "Bearer " 前缀一起复制过来的, + * 如果不做清理,后续 header.set("Authorization", "Bearer " + token) 会拼出 + * "Bearer Bearer xxx",导致123网盘接口报错 tokenstring should not contain 'bearer '。 + */ + private String stripBearerPrefix(String token) { + if (StringUtils.isBlank(token)) { return token; } - return auths.get("authorization"); + String trimmed = token.trim(); + if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) { + trimmed = trimmed.substring(7).trim(); + } + return trimmed; } private boolean isTokenExpired() { diff --git a/web-front/src/views/Home.vue b/web-front/src/views/Home.vue index f095ce5..55fce9f 100644 --- a/web-front/src/views/Home.vue +++ b/web-front/src/views/Home.vue @@ -326,6 +326,10 @@ 蓝奏优享 (IZ) 大文件 + + 123云盘 (YE) + 部分分享需登录 + @@ -767,7 +771,8 @@ export default { if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC' if (url.includes('feijipan.com') || url.includes('feijihe.com') || url.includes('xiaofeiyang.com')) return 'FJ' if (url.includes('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ' - if (url.includes('123pan.com') || url.includes('123684.com') || url.includes('123865.com')) return 'YE' + // 123网盘域名较多(如 123pan.com/123pan.cn/share.123pan.cn/123684.com/123865.com 等数字域名),使用规则匹配代替枚举 + if (/123\d{3}\.com|123panpay\.com|123pan\.(?:com|cn)/.test(url)) return 'YE' return '' }, @@ -789,7 +794,8 @@ export default { 'QK': '夸克网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)', 'UC': 'UC网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)', 'FJ': '小飞机网盘大文件(>100MB)需要配置认证信息', - 'IZ': '蓝奏优享大文件需要配置认证信息' + 'IZ': '蓝奏优享大文件需要配置认证信息', + 'YE': '123云盘部分分享(需要登录才能查看/下载)需要配置账号密码或 Authorization Token' } return hints[this.authConfig.panType] || '请选择网盘类型后配置认证信息' }, @@ -1230,10 +1236,11 @@ export default { duration: 5000, showClose: true }) - } else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') { - // 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证 + } else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le' || panType === 'ye') { + // 小飞机、蓝奏、优享、联想乐云、123云盘:提示大文件/需登录分享需要认证 const hasAuth = this.allAuthConfigs[panType]?.cookie || this.allAuthConfigs[panType]?.username || + this.allAuthConfigs[panType]?.token || (this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0 if (!hasAuth) { this.$message.info({ @@ -1612,12 +1619,14 @@ export default { this.donateSubmitting = true try { + // 只提交当前认证方式实际用到的字段,避免切换认证类型后遗留的用户名/密码脏数据被一起提交 + const isPasswordAuth = this.donateConfig.authType === 'password' const payload = { panType: this.donateConfig.panType, authType: this.donateConfig.authType, - username: this.donateConfig.username || '', - password: this.donateConfig.password || '', - token: this.donateConfig.token || '', + username: isPasswordAuth ? (this.donateConfig.username || '') : '', + password: isPasswordAuth ? (this.donateConfig.password || '') : '', + token: isPasswordAuth ? '' : (this.donateConfig.token || ''), remark: this.donateConfig.remark || '' } await axios.post(`${this.baseAPI}/v2/donateAccount`, payload) diff --git a/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java b/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java index f761094..a640eb3 100644 --- a/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java +++ b/web-service/src/main/java/cn/qaiu/lz/common/util/URLParamUtil.java @@ -100,21 +100,27 @@ public class URLParamUtil { } } if (localMap.containsKey(ConfigConstant.AUTHS)) { - JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS); - if (auths.containsKey(type)) { - // 需要处理引号 - MultiMap entries = MultiMap.caseInsensitiveMultiMap(); - JsonObject jsonObject = auths.getJsonObject(type); - if (jsonObject != null) { - jsonObject.forEach(entity -> { - if (entity == null || entity.getValue() == null) { - return; - } - entries.set(entity.getKey(), entity.getValue().toString()); - }); - } + // 如果本次请求已经通过 auth 临时参数(个人配置/捐赠账号)设置过认证信息, + // 则不要再用后台 app-dev.yml 的静态配置覆盖,否则临时认证会被静默清空/替换为空配置。 + boolean tempAuthAdded = Boolean.TRUE.equals( + parserCreate.getShareLinkInfo().getOtherParam().get("__TEMP_AUTH_ADDED")); + if (!tempAuthAdded) { + JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS); + if (auths.containsKey(type)) { + // 需要处理引号 + MultiMap entries = MultiMap.caseInsensitiveMultiMap(); + JsonObject jsonObject = auths.getJsonObject(type); + if (jsonObject != null) { + jsonObject.forEach(entity -> { + if (entity == null || entity.getValue() == null) { + return; + } + entries.set(entity.getKey(), entity.getValue().toString()); + }); + } - parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries); + parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries); + } } } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java index 8b8a303..6a8cb73 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/CacheServiceImpl.java @@ -8,15 +8,22 @@ import cn.qaiu.lz.common.cache.CacheTotalField; import cn.qaiu.lz.common.util.URLParamUtil; import cn.qaiu.lz.web.model.CacheLinkInfo; import cn.qaiu.lz.web.service.CacheService; +import cn.qaiu.lz.web.service.DbService; import cn.qaiu.parser.IPanTool; import cn.qaiu.parser.ParserCreate; import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory; import cn.qaiu.parser.clientlink.ClientLinkType; import cn.qaiu.vx.core.annotaions.Service; +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.MultiMap; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; +import io.vertx.core.shareddata.LocalMap; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Date; @@ -28,8 +35,11 @@ import java.util.Map; public class CacheServiceImpl implements CacheService { private static final String SKIP_CLIENT_LINKS = "_skipClientLinks"; + 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 DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); static { // 服务类加载时注册缓存定时清理任务 @@ -71,7 +81,9 @@ public class CacheServiceImpl implements CacheService { promise.fail(cause); return; } - IPanTool.closeAfter(tool, tool::parse).onSuccess(redirectUrl -> { + IPanTool.closeAfter(tool, tool::parse).onFailure(err -> { + recordAutoDonatedFailureIfNeeded(shareLinkInfo, err); + }).onSuccess(redirectUrl -> { // 使用 effectiveCacheDuration long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L; result.setDirectLink(redirectUrl); @@ -141,6 +153,124 @@ public class CacheServiceImpl implements CacheService { return DateFormatUtils.format(new Date(ts), "yyyy-MM-dd HH:mm:ss"); } + /** + * 当本次请求没有携带临时认证参数(auth=xxx),且该网盘类型在 app-dev.yml 中也没有配置可用的 + * 静态账号密码/token 时,自动从捐赠账号池中随机挑选一个可用账号使用, + * 这样捐赠者捐赠账号后,普通请求(不带 auth 参数)也能直接受益,无需前端额外拼接参数。 + */ + private Future applyDonatedAccountFallback(ParserCreate parserCreate) { + ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo(); + Map 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.succeededFuture(); + }) + .recover(err -> { + log.warn("自动获取捐赠账号失败: type={}", type, err); + return Future.succeededFuture(); + }); + } + + /** + * 检查 app-dev.yml 中该网盘类型是否已配置了可用的静态认证信息(非空的 username/password/token/authorization 等)。 + * 如果已配置,则不需要再自动回退到捐赠账号池。 + */ + private boolean hasUsableStaticAuthConfig(String type) { + LocalMap 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) { if (shareLinkInfo == null || shareLinkInfo.getOtherParam() == null) { return true; @@ -279,7 +409,10 @@ public class CacheServiceImpl implements CacheService { return Future.failedFuture(e); } parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap()); - return getAndSaveCachedShareLink(parserCreate); + + ParserCreate finalParserCreate = parserCreate; + return applyDonatedAccountFallback(finalParserCreate) + .compose(v -> getAndSaveCachedShareLink(finalParserCreate)); } @Override @@ -305,7 +438,9 @@ public class CacheServiceImpl implements CacheService { otherParam.getString("authInfo4"), otherParam.getString("authInfo5")); } - - return getAndSaveCachedShareLink(parserCreate); + + ParserCreate finalParserCreate = parserCreate; + return applyDonatedAccountFallback(finalParserCreate) + .compose(v -> getAndSaveCachedShareLink(finalParserCreate)); } } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java index dae80f3..fbf88a1 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/service/impl/DbServiceImpl.java @@ -325,9 +325,16 @@ public class DbServiceImpl implements DbService { public Future saveDonatedAccount(JsonObject account) { JDBCPool client = JDBCPoolInit.instance().getPool(); - Future encryptedUsername = CryptoUtil.encrypt(account.getString("username")); - Future encryptedPassword = CryptoUtil.encrypt(account.getString("password")); - Future encryptedToken = CryptoUtil.encrypt(account.getString("token")); + // 只保留当前认证方式实际用到的字段,避免调用方切换认证类型后遗留的用户名/密码脏数据 + // 被一并存入库中,导致后续解析时被误当作真实凭证使用(例如把废弃的用户名当手机号登录)。 + boolean isPasswordAuth = "password".equalsIgnoreCase(account.getString("authType")); + String usernameToStore = isPasswordAuth ? account.getString("username") : null; + String passwordToStore = isPasswordAuth ? account.getString("password") : null; + String tokenToStore = isPasswordAuth ? null : account.getString("token"); + + Future encryptedUsername = CryptoUtil.encrypt(usernameToStore); + Future encryptedPassword = CryptoUtil.encrypt(passwordToStore); + Future encryptedToken = CryptoUtil.encrypt(tokenToStore); return ensureFailCountColumn(client).compose(v -> Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> { @@ -412,6 +419,14 @@ public class DbServiceImpl implements DbService { String password = passwordFuture.result(); String token = tokenFuture.result(); + // 历史脏数据兜底:非 password 认证类型的账号不应该带用户名/密码 + // (例如切换认证类型前遗留的表单数据),否则会被解析器误当作真实账号密码去登录。 + boolean isPasswordAuth = "password".equalsIgnoreCase(row.getString("auth_type")); + if (!isPasswordAuth) { + username = null; + password = null; + } + // 如果解密后没有任何可用凭证,返回空对象,避免把密文当作明文认证参数下发给前端 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"));