Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 8bd9830e04 Support Authorization field with ****** stripping in Ye2Tool 2026-07-07 02:36:05 +00:00
copilot-swe-agent[bot] 2aeb64b87c Initial plan 2026-07-07 02:29:42 +00:00
13 changed files with 316 additions and 417 deletions
+26 -13
View File
@@ -158,19 +158,37 @@ jobs:
path: web-service/target/package path: web-service/target/package
# ============================================================ # ============================================================
# 选择 JDK 模块 # jdeps 分析 → 确定所需 JDK 模块
# ============================================================ # ============================================================
- name: 选择完整 JDK 模块集合 - name: 分析所需 JDK 模块jdeps
run: | run: |
# 123 网盘/自定义 JS 解析器会通过 javax.script、Nashorn 和 SPI/反射加载模块, MAIN_JAR="web-service/target/package/netdisk-fast-download.jar"
# jdeps 无法稳定识别这些运行时依赖。这里优先保证原生包可运行,使用完整 LIB_DIR="web-service/target/package/lib"
# JDK 模块集合生成运行时,避免 Windows 精简 JRE 缺 java.scripting/jdk.dynalink 等模块。
printf 'JDK_MODULES=ALL-MODULE-PATH\n' >> $GITHUB_ENV CP=""
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" \
@@ -197,11 +215,6 @@ 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@'
# ============================================================ # ============================================================
# 组装包目录 # 组装包目录
# ============================================================ # ============================================================
+2 -37
View File
@@ -6,10 +6,8 @@ QQ交流群:1017480890
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a> <a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a>
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a> <a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a> <a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
<a href="https://github.com/QAIU/netdisk-fast-download">
<img src="https://img.shields.io/github/stars/QAIU/netdisk-fast-download?style=flat&logo=github" alt="GitHub Stars">
</a>
<a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a> <a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a>
<a href="https://oosmetrics.com/repo/qaiu/netdisk-fast-download"><img src="https://api.oosmetrics.com/api/v1/badge/achievement/826aa27a-6e59-4de5-b7fa-cd189f484035.svg"></a>
<p align="center"> <p align="center">
<a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p> </p>
@@ -69,7 +67,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
- [移动云云空间-ec](https://www.ecpan.cn/web) - [移动云云空间-ec](https://www.ecpan.cn/web)
- [小飞机网盘-fj](https://www.feijipan.com/) - [小飞机网盘-fj](https://www.feijipan.com/)
- [亿方云-fc](https://www.fangcloud.com/) - [亿方云-fc](https://www.fangcloud.com/)
- [123云盘-ye](https://www.123pan.com/) ⚠️仅建议本地部署使用,需登录认证,公共/云端服务器风控严格建议自行部署(Windows 可直接用 run.bat 一键运行) - [123云盘-ye](https://www.123pan.com/)
- ~[115网盘(失效)-p115](https://115.com/)~ - ~[115网盘(失效)-p115](https://115.com/)~
- [文叔叔-ws](https://www.wenshushu.cn/) - [文叔叔-ws](https://www.wenshushu.cn/)
- [联想乐云-le](https://lecloud.lenovo.com/) - [联想乐云-le](https://lecloud.lenovo.com/)
@@ -176,7 +174,6 @@ GET /json/getFileList?url={分享链接}&pwd={密码}
| UC网盘(UC) | **必须** | 必须配置 Cookie 才能解析 | | UC网盘(UC) | **必须** | 必须配置 Cookie 才能解析 |
| 小飞机网盘(FJ) | 可选 | 大文件(>100MB)需要认证 | | 小飞机网盘(FJ) | 可选 | 大文件(>100MB)需要认证 |
| 蓝奏优享(IZ) | 可选 | 大文件需要认证 | | 蓝奏优享(IZ) | 可选 | 大文件需要认证 |
| 123网盘(YE) | 可选 | 需要下载大文件/需要登录的分享时才需要认证,支持账号密码或 token/authorization |
**使用示例** **使用示例**
``` ```
@@ -197,38 +194,6 @@ GET /parser?url={分享链接}&pwd={密码}&auth={加密后的认证参数}
- 目的:防止客户端伪造失败计数请求 - 目的:防止客户端伪造失败计数请求
- 建议:使用高强度随机字符串,且不要与 `authEncryptKey` 相同 - 建议:使用高强度随机字符串,且不要与 `authEncryptKey` 相同
#### `auth` 临时认证参数 与 `auths` 静态配置认证 的区别
本项目存在两种互相独立的认证配置方式,作用范围不同,不要混淆:
| 方式 | 配置位置 | 生效范围 | 适用场景 |
|------|---------|---------|---------|
| `auth` 临时认证参数 | 请求 URL 上的 `auth` 查询参数 | **仅当次请求**,优先级高于 app-dev.yml 中的静态配置 | 调用方按用户临时提供的账号/Cookie/token 解析,不同请求可携带不同认证信息 |
| `auths` 静态配置认证 | `app-dev.yml``auths.<网盘标识>` 节点 | **服务端长期生效**,所有未携带 `auth` 参数的请求都会复用 | 部署方自己长期配置一份账号,供所有请求默认使用 |
以 123网盘(`ye`)为例,`app-dev.yml` 中支持以下几种写法(三选一即可):
```yaml
auths:
ye:
username: 你的123网盘账号
password: 你的123网盘密码
```
```yaml
auths:
ye:
token: 已登录后获取的 Authorization/AccessToken
```
```yaml
auths:
ye:
authorization: 已登录后获取的 Authorization/AccessToken # 与 token 等价,二选一
```
> ⚠️ 注意:YAML 中 key 后面不写值(如 `authorization:` 空着)等同于没配置,不会生效,必须填入真实的账号密码或 token 内容。
如果只是临时调用一次,不想改动服务端配置,也可以用上面提到的 `auth` 参数临时传递(`authType` 可选 `password`/`accesstoken`/`authorization`),无需重启服务,仅本次请求生效。
### 特殊说明 ### 特殊说明
- 移动云云空间的 `分享key` 取分享链接中的 `data` 参数值 - 移动云云空间的 `分享key` 取分享链接中的 `data` 参数值
@@ -79,48 +79,13 @@ public class Ye2Tool extends PanBase {
accountId = auths.get("_configId"); accountId = auths.get("_configId");
} else if (auths.contains("username")) { } else if (auths.contains("username")) {
accountId = auths.get("username"); accountId = auths.get("username");
} else { } else if (auths.contains("token")) {
String token = resolveProvidedToken(auths); String token = auths.get("token");
if (StringUtils.isNotEmpty(token)) { accountId = token.substring(0, Math.min(16, token.length()));
accountId = token.substring(0, Math.min(16, token.length()));
}
} }
return accountId; return accountId;
} }
/**
* 从配置/临时认证参数中取出可直接使用的 Bearer token。
* 兼容两种 key
* - tokenURLParamUtil 处理 auth= 临时参数(authType=accesstoken/authorization)时写入的字段名
* - authorizationapp-dev.yml 静态配置中更符合直觉的写法(auths.ye.authorization: xxx
*/
private String resolveProvidedToken(MultiMap auths) {
if (auths == null) {
return null;
}
String token = auths.get("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;
}
String trimmed = token.trim();
if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) {
trimmed = trimmed.substring(7).trim();
}
return trimmed;
}
private boolean isTokenExpired() { private boolean isTokenExpired() {
return TokenCache.isExpired(cacheKey); return TokenCache.isExpired(cacheKey);
} }
@@ -224,14 +189,15 @@ public class Ye2Tool extends PanBase {
private Future<String> resolveTokenFuture() { private Future<String> resolveTokenFuture() {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths"); MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null) { if (auths != null) {
// 当同时提供了用户名+密码时,优先走真实登录流程。 String raw = null;
// 注:前端/auth参数中 authType=password|username_password 时,URLParamUtil 出于兼容旧解析器的目的 if (auths.contains("token")) {
// 会把 authToken(用户名) 同时写入 token 字段,如果这里无条件信任 token,会把用户名当成 raw = auths.get("token");
// Bearer token 使用,导致真实的账号密码登录被跳过。 } else if (auths.contains("Authorization")) {
boolean hasCredential = StringUtils.isNotEmpty(auths.get("username")) raw = auths.get("Authorization");
&& StringUtils.isNotEmpty(auths.get("password")); }
String providedToken = resolveProvidedToken(auths); String providedToken = normalizeBearerToken(raw);
if (!hasCredential && StringUtils.isNotEmpty(providedToken)) { if (StringUtils.isNotEmpty(providedToken)) {
log.info("使用配置token,前8位: {}...", providedToken.substring(0, Math.min(8, providedToken.length())));
TokenCache.putToken(cacheKey, providedToken); TokenCache.putToken(cacheKey, providedToken);
return Future.succeededFuture(providedToken); return Future.succeededFuture(providedToken);
} }
@@ -244,6 +210,17 @@ public class Ye2Tool extends PanBase {
return Future.succeededFuture(cached); return Future.succeededFuture(cached);
} }
private String normalizeBearerToken(String raw) {
if (StringUtils.isBlank(raw)) {
return null;
}
String token = raw.trim();
if (token.regionMatches(true, 0, "Bearer ", 0, 7)) {
token = token.substring(7).trim();
}
return StringUtils.isNotEmpty(token) ? token : null;
}
private Future<String> loginAndGetToken() { private Future<String> loginAndGetToken() {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths"); MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths == null) { if (auths == null) {
@@ -641,14 +618,7 @@ public class Ye2Tool extends PanBase {
return; return;
} }
String rawShareKey = paramJson.getString("shareKey"); String normalizedShareKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey());
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)
@@ -0,0 +1,140 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import io.vertx.core.MultiMap;
import java.util.HashMap;
import java.util.Map;
/**
* Ye2Tool token 解析验证测试
* 验证 Authorization 字段和 ******
*/
public class Ye2ToolTokenValidationTest {
public static void main(String[] args) {
System.out.println("========================================");
System.out.println(" Ye2Tool Token 解析验证测试");
System.out.println("========================================\n");
testNormalizeBearerToken();
testYe2ToolWithTokenField();
testYe2ToolWithAuthorizationField();
testYe2ToolWithoutAuth();
System.out.println("\n========================================");
System.out.println(" 所有验证通过! ✓");
System.out.println("========================================");
}
/** 独立验证 normalizeBearerToken 逻辑(内联实现与 Ye2Tool 保持一致)*/
private static String normalizeBearerToken(String raw) {
if (raw == null || raw.trim().isEmpty()) {
return null;
}
String token = raw.trim();
if (token.regionMatches(true, 0, "Bearer ", 0, 7)) {
token = token.substring(7).trim();
}
return token.isEmpty() ? null : token;
}
private static void testNormalizeBearerToken() {
System.out.println("=== 验证 normalizeBearerToken 逻辑 ===");
assertNormalize("testtoken123abc", "testtoken123abc", "纯 token 不变");
assertNormalize("Bearer testtoken123abc", "testtoken123abc", "******");
assertNormalize("bearer testtoken123abc", "testtoken123abc", "小写 bearer 前缀被去掉");
assertNormalize("BEARER testtoken123abc", "testtoken123abc", "大写 BEARER 前缀被去掉");
assertNormalize(" Bearer testtoken123abc ", "testtoken123abc", "前后空格被 trim");
assertNormalizeNull(null, "null 输入返回 null");
assertNormalizeNull("", "空字符串返回 null");
assertNormalizeNull(" ", "纯空格返回 null");
System.out.println();
}
private static void assertNormalize(String input, String expected, String desc) {
String actual = normalizeBearerToken(input);
if (!expected.equals(actual)) {
throw new AssertionError("FAIL [" + desc + "]: expected='" + expected + "', actual='" + actual + "'");
}
System.out.println("" + desc);
}
private static void assertNormalizeNull(String input, String desc) {
String actual = normalizeBearerToken(input);
if (actual != null) {
throw new AssertionError("FAIL [" + desc + "]: expected null, actual='" + actual + "'");
}
System.out.println("" + desc);
}
private static void testYe2ToolWithTokenField() {
System.out.println("=== 测试 Ye2Tooltoken 字段)===");
try {
MultiMap auths = MultiMap.caseInsensitiveMultiMap();
auths.set("token", "testtoken123abc");
Map<String, Object> otherParam = new HashMap<>();
otherParam.put("auths", auths);
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type("YE")
.panName("123网盘")
.shareKey("test_key_token")
.shareUrl("https://www.123pan.com/s/test123")
.build();
shareLinkInfo.setOtherParam(otherParam);
Ye2Tool ye2Tool = new Ye2Tool(shareLinkInfo);
System.out.println("✓ Ye2Tool 实例创建成功(token 字段)\n");
} catch (Exception e) {
System.err.println("✗ Ye2Tooltoken 字段)测试失败: " + e.getMessage());
e.printStackTrace();
}
}
private static void testYe2ToolWithAuthorizationField() {
System.out.println("=== 测试 Ye2ToolAuthorization 字段,带 Bearer===");
try {
MultiMap auths = MultiMap.caseInsensitiveMultiMap();
auths.set("Authorization", "Bearer testtoken123abc");
Map<String, Object> otherParam = new HashMap<>();
otherParam.put("auths", auths);
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type("YE")
.panName("123网盘")
.shareKey("test_key_auth")
.shareUrl("https://www.123pan.com/s/test456")
.build();
shareLinkInfo.setOtherParam(otherParam);
Ye2Tool ye2Tool = new Ye2Tool(shareLinkInfo);
System.out.println("✓ Ye2Tool 实例创建成功(Authorization 字段)\n");
} catch (Exception e) {
System.err.println("✗ Ye2ToolAuthorization 字段)测试失败: " + e.getMessage());
e.printStackTrace();
}
}
private static void testYe2ToolWithoutAuth() {
System.out.println("=== 测试 Ye2Tool(无认证配置)===");
try {
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type("YE")
.panName("123网盘")
.shareKey("test_key_noauth")
.shareUrl("https://www.123pan.com/s/test789")
.build();
Ye2Tool ye2Tool = new Ye2Tool(shareLinkInfo);
System.out.println("✓ Ye2Tool 实例创建成功(无认证)\n");
} catch (Exception e) {
System.err.println("✗ Ye2Tool(无认证)测试失败: " + e.getMessage());
e.printStackTrace();
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
</modules> </modules>
<properties> <properties>
<revision>0.4.1</revision> <revision>0.3.4</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.4.1", "version": "0.2.1",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
+7 -16
View File
@@ -326,10 +326,6 @@
<span>蓝奏优享 (IZ)</span> <span>蓝奏优享 (IZ)</span>
<el-tag size="small" type="warning" style="margin-left: 8px">大文件</el-tag> <el-tag size="small" type="warning" style="margin-left: 8px">大文件</el-tag>
</el-option> </el-option>
<el-option label="123云盘 (YE)" value="YE">
<span>123云盘 (YE)</span>
<el-tag size="small" type="warning" style="margin-left: 8px">部分分享需登录</el-tag>
</el-option>
</el-option-group> </el-option-group>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -771,8 +767,7 @@ export default {
if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC' 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('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('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ'
// 123网盘域名较多(如 123pan.com/123pan.cn/share.123pan.cn/123684.com/123865.com 等数字域名),使用规则匹配代替枚举 if (url.includes('123pan.com') || url.includes('123684.com') || url.includes('123865.com')) return 'YE'
if (/123\d{3}\.com|123panpay\.com|123pan\.(?:com|cn)/.test(url)) return 'YE'
return '' return ''
}, },
@@ -794,8 +789,7 @@ export default {
'QK': '夸克网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)', 'QK': '夸克网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
'UC': 'UC网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)', 'UC': 'UC网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
'FJ': '小飞机网盘大文件(>100MB)需要配置认证信息', 'FJ': '小飞机网盘大文件(>100MB)需要配置认证信息',
'IZ': '蓝奏优享大文件需要配置认证信息', 'IZ': '蓝奏优享大文件需要配置认证信息'
'YE': '123云盘部分分享(需要登录才能查看/下载)需要配置账号密码或 Authorization Token'
} }
return hints[this.authConfig.panType] || '请选择网盘类型后配置认证信息' return hints[this.authConfig.panType] || '请选择网盘类型后配置认证信息'
}, },
@@ -1236,11 +1230,10 @@ export default {
duration: 5000, duration: 5000,
showClose: true showClose: true
}) })
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le' || panType === 'ye') { } else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
// 小飞机、蓝奏、优享、联想乐云、123云盘:提示大文件/需登录分享需要认证 // 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
const hasAuth = this.allAuthConfigs[panType]?.cookie || const hasAuth = this.allAuthConfigs[panType]?.cookie ||
this.allAuthConfigs[panType]?.username || this.allAuthConfigs[panType]?.username ||
this.allAuthConfigs[panType]?.token ||
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0 (this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
if (!hasAuth) { if (!hasAuth) {
this.$message.info({ this.$message.info({
@@ -1619,14 +1612,12 @@ export default {
this.donateSubmitting = true this.donateSubmitting = true
try { try {
// 只提交当前认证方式实际用到的字段,避免切换认证类型后遗留的用户名/密码脏数据被一起提交
const isPasswordAuth = this.donateConfig.authType === 'password'
const payload = { const payload = {
panType: this.donateConfig.panType, panType: this.donateConfig.panType,
authType: this.donateConfig.authType, authType: this.donateConfig.authType,
username: isPasswordAuth ? (this.donateConfig.username || '') : '', username: this.donateConfig.username || '',
password: isPasswordAuth ? (this.donateConfig.password || '') : '', password: this.donateConfig.password || '',
token: isPasswordAuth ? '' : (this.donateConfig.token || ''), token: this.donateConfig.token || '',
remark: this.donateConfig.remark || '' remark: this.donateConfig.remark || ''
} }
await axios.post(`${this.baseAPI}/v2/donateAccount`, payload) await axios.post(`${this.baseAPI}/v2/donateAccount`, payload)
@@ -1,203 +0,0 @@
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("认证");
}
}
@@ -100,27 +100,21 @@ public class URLParamUtil {
} }
} }
if (localMap.containsKey(ConfigConstant.AUTHS)) { if (localMap.containsKey(ConfigConstant.AUTHS)) {
// 如果本次请求已经通过 auth 临时参数(个人配置/捐赠账号)设置过认证信息, JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
// 则不要再用后台 app-dev.yml 的静态配置覆盖,否则临时认证会被静默清空/替换为空配置。 if (auths.containsKey(type)) {
boolean tempAuthAdded = Boolean.TRUE.equals( // 需要处理引号
parserCreate.getShareLinkInfo().getOtherParam().get("__TEMP_AUTH_ADDED")); MultiMap entries = MultiMap.caseInsensitiveMultiMap();
if (!tempAuthAdded) { JsonObject jsonObject = auths.getJsonObject(type);
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS); if (jsonObject != null) {
if (auths.containsKey(type)) { jsonObject.forEach(entity -> {
// 需要处理引号 if (entity == null || entity.getValue() == null) {
MultiMap entries = MultiMap.caseInsensitiveMultiMap(); return;
JsonObject jsonObject = auths.getJsonObject(type); }
if (jsonObject != null) { entries.set(entity.getKey(), entity.getValue().toString());
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);
} }
} }
@@ -5,7 +5,6 @@ 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;
@@ -160,8 +159,7 @@ 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 {
@@ -170,7 +168,6 @@ 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)) {
@@ -179,22 +176,13 @@ public class ParserApi {
if (StringUtils.isNotBlank(uuid)) { if (StringUtils.isNotBlank(uuid)) {
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid); parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
} }
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService) IPanTool tool = parserCreate.createTool();
.compose(v -> { return IPanTool.closeAfter(tool, tool::parseFileList);
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, String auth) { public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) {
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
.setShareLinkInfoPwd("-"); .setShareLinkInfoPwd("-");
@@ -210,28 +198,17 @@ 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);
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService) IPanTool tool = parserCreate.createTool();
.compose(v -> { return IPanTool.closeAfter(tool, tool::parseById);
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, auth) getFileDownUrl(request, type, param)
.onSuccess(res -> { .onSuccess(res -> {
ResponseUtil.redirect(response, res, promise); ResponseUtil.redirect(response, res, promise);
}) })
@@ -306,12 +283,11 @@ 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, auth) getFileDownUrl(request, type, param)
.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,7 +1,8 @@
package cn.qaiu.lz.web.controller; package cn.qaiu.lz.web.controller;
import cn.qaiu.lz.common.util.ParserAuthUtil; import cn.qaiu.lz.common.util.AuthParamCodec;
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;
@@ -28,7 +29,7 @@ import lombok.extern.slf4j.Slf4j;
@RouteHandler("/") @RouteHandler("/")
public class ServerApi { public class ServerApi {
private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS; private static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
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);
@@ -46,7 +47,7 @@ public class ServerApi {
addCacheHeaders(response, res), addCacheHeaders(response, res),
res.getDirectLink(), promise)) res.getDirectLink(), promise))
.onFailure(t -> { .onFailure(t -> {
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t); recordDonatedAccountFailureIfNeeded(otherParam, t);
promise.tryFail(t); promise.tryFail(t);
}); });
return promise.future(); return promise.future();
@@ -57,14 +58,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 -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t)); .onFailure(t -> recordDonatedAccountFailureIfNeeded(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 -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t)); .onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t));
} }
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET) @RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
@@ -145,6 +146,66 @@ public class ServerApi {
} }
private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) { private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) {
return ParserAuthUtil.buildOtherParam(request, auth, resolveOrigin(request), skipClientLinks); JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent"), "_requestOrigin", resolveOrigin(request));
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,17 +5,14 @@ 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;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.IPanTool; import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate; import cn.qaiu.parser.ParserCreate;
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory; 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 io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
@@ -30,10 +27,9 @@ import java.util.Map;
@Slf4j @Slf4j
public class CacheServiceImpl implements CacheService { public class CacheServiceImpl implements CacheService {
private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS; private static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
private final CacheManager cacheManager = new CacheManager(); private final CacheManager cacheManager = new CacheManager();
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
static { static {
// 服务类加载时注册缓存定时清理任务 // 服务类加载时注册缓存定时清理任务
@@ -75,9 +71,7 @@ public class CacheServiceImpl implements CacheService {
promise.fail(cause); promise.fail(cause);
return; return;
} }
IPanTool.closeAfter(tool, tool::parse).onFailure(err -> { IPanTool.closeAfter(tool, tool::parse).onSuccess(redirectUrl -> {
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService, shareLinkInfo, err);
}).onSuccess(redirectUrl -> {
// 使用 effectiveCacheDuration // 使用 effectiveCacheDuration
long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L; long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L;
result.setDirectLink(redirectUrl); result.setDirectLink(redirectUrl);
@@ -284,9 +278,8 @@ public class CacheServiceImpl implements CacheService {
} catch (Exception e) { } catch (Exception e) {
return Future.failedFuture(e); return Future.failedFuture(e);
} }
ParserCreate finalParserCreate = parserCreate; parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService) return getAndSaveCachedShareLink(parserCreate);
.compose(v -> getAndSaveCachedShareLink(finalParserCreate));
} }
@Override @Override
@@ -297,8 +290,22 @@ public class CacheServiceImpl implements CacheService {
} catch (Exception e) { } catch (Exception e) {
return Future.failedFuture(e); return Future.failedFuture(e);
} }
ParserCreate finalParserCreate = parserCreate; parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService)
.compose(v -> getAndSaveCachedShareLink(finalParserCreate)); // 检查是否有临时认证参数
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"));
}
return getAndSaveCachedShareLink(parserCreate);
} }
} }
@@ -325,16 +325,9 @@ public class DbServiceImpl implements DbService {
public Future<JsonObject> saveDonatedAccount(JsonObject account) { public Future<JsonObject> saveDonatedAccount(JsonObject account) {
JDBCPool client = JDBCPoolInit.instance().getPool(); JDBCPool client = JDBCPoolInit.instance().getPool();
// 只保留当前认证方式实际用到的字段,避免调用方切换认证类型后遗留的用户名/密码脏数据 Future<String> encryptedUsername = CryptoUtil.encrypt(account.getString("username"));
// 被一并存入库中,导致后续解析时被误当作真实凭证使用(例如把废弃的用户名当手机号登录)。 Future<String> encryptedPassword = CryptoUtil.encrypt(account.getString("password"));
boolean isPasswordAuth = "password".equalsIgnoreCase(account.getString("authType")); Future<String> encryptedToken = CryptoUtil.encrypt(account.getString("token"));
String usernameToStore = isPasswordAuth ? account.getString("username") : null;
String passwordToStore = isPasswordAuth ? account.getString("password") : null;
String tokenToStore = isPasswordAuth ? null : account.getString("token");
Future<String> encryptedUsername = CryptoUtil.encrypt(usernameToStore);
Future<String> encryptedPassword = CryptoUtil.encrypt(passwordToStore);
Future<String> encryptedToken = CryptoUtil.encrypt(tokenToStore);
return ensureFailCountColumn(client).compose(v -> return ensureFailCountColumn(client).compose(v ->
Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> { Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> {
@@ -419,14 +412,6 @@ public class DbServiceImpl implements DbService {
String password = passwordFuture.result(); String password = passwordFuture.result();
String token = tokenFuture.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)) { 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")); log.warn("random donated account has no usable credential after decrypt, accountId={}", row.getLong("id"));