Compare commits

...

15 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
625b7c9291 Address Wenshushu parser review feedback
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/8f8b8ba9-4a66-4757-a7f7-4ab342da729e

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-05-09 04:04:13 +00:00
copilot-swe-agent[bot]
3819bc4bbd Document Wenshushu sign payload change
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/8f8b8ba9-4a66-4757-a7f7-4ab342da729e

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-05-09 04:02:37 +00:00
copilot-swe-agent[bot]
97b0dd2029 Refine Wenshushu file id handling
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/8f8b8ba9-4a66-4757-a7f7-4ab342da729e

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-05-09 04:00:51 +00:00
copilot-swe-agent[bot]
20cca4c458 Update Wenshushu sign API request
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/8f8b8ba9-4a66-4757-a7f7-4ab342da729e

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-05-09 04:00:26 +00:00
q
a5fc41f152 1 2026-04-29 22:40:57 +08:00
q
3245a27156 release v3.0.2 2026-04-29 22:37:07 +08:00
q
ce0fbf65aa fixed test docx 2026-04-28 15:57:50 +08:00
qaiu
eb87c3d865 Update app.properties 2026-04-27 20:06:19 +08:00
qaiu
990a90e461 更新 README.md 2026-04-27 12:04:29 +08:00
qaiu
45cb255a4d 更新 README.md 2026-04-27 12:01:11 +08:00
qaiu
5e8b8a3575 更新 README.md 2026-04-24 03:05:29 +08:00
qaiu
c317e53c24 Merge pull request #182 from qaiu/copilot/update-readme-netdisk-comparison
README: Remove 奶牛快传(cowtransfer) references
2026-04-23 19:41:33 +08:00
copilot-swe-agent[bot]
29b59d8450 Remove 奶牛快传(cowtransfer) from README.md
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/63030ac7-158c-43ca-9dcd-254e03c6f8f4

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-23 11:27:04 +00:00
qaiu
97b7e2f86e Merge pull request #181 from qaiu/copilot/check-authentication-mechanism
fix: 演练场密码登录后所有API返回"未授权访问"
2026-04-23 09:34:18 +08:00
copilot-swe-agent[bot]
2f55294b58 fix: 修复演练场输入密码后提示未授权访问的问题
根本原因:框架 RouterHandlerFactory 未注册 SessionHandler,
导致 ctx.session() 始终返回 null。登录时密码校验通过但认证
状态被静默丢弃,后续所有请求均返回"未授权访问"。

修复方案:将 Session 鉴权改为 Token(Bearer)鉴权:
- PlaygroundConfig: 新增 generateToken()/validateToken(),
  使用 SecureRandom 生成密码学安全 Token,并在生成时
  清理过期 Token 防止内存泄漏
- PlaygroundApi: login() 返回 Token;checkAuth() 从
  Authorization 请求头中读取并校验 Token
- playgroundApi.js: 添加请求拦截器自动携带 Token;
  login() 从响应中提取并保存 Token 到 localStorage
- Playground.vue: 后端报告未认证时同步清除 playground_token

Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/52144d13-cd49-4a3d-b279-9b8d6cbad757

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-23 01:22:09 +00:00
13 changed files with 110 additions and 55 deletions

View File

@@ -3,27 +3,26 @@ QQ交流群1017480890
<p align="center"> <p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a> <a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a> <a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.22-blue?style=flat"></a> <a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.24-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://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>
<div align="center" style="display:flex; justify-content:center; gap:10px; align-items:flex-start;">
<img
src="https://github.com/user-attachments/assets/bf266d0a-aaf8-4772-9231-e38a4b7bb6cb"
alt="image1"
style="width:300px; max-width:300px; flex:none;"
>
<img
src="https://github.com/user-attachments/assets/bb7a85f0-c256-4b4a-a11b-3ceb55afc302"
alt="image2"
style="width:300px; max-width:300px; flex:none;"
>
</div>
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链可广泛应用于各类下载站资源站个人博客图床APP下载更新视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。 ![alt text](web-front/img/image.png)
## 国内镜像
本项目同步托管于 **AtomGit**,国内访问更流畅:👉 [https://atomgit.com/QAIU/netdisk-fast-download](https://atomgit.com/QAIU/netdisk-fast-download)
## 介绍
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链可广泛应用于各类下载站资源站个人博客图床APP下载更新视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接已支持蓝奏云/蓝奏云优享/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
[官方文档](https://nfd-parser.github.io/) [官方文档](https://nfd-parser.github.io/)
[API接入](https://nfdparser.apifox.cn/) [API接入](https://nfdparser.apifox.cn/)
@@ -62,7 +61,6 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
**注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效。** **注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效。**
## 网盘支持情况: ## 网盘支持情况:
> 20230905 奶牛云直链做了防盗链需加入请求头Referer: https://cowtransfer.com/
> 20230824 123云盘解析大文件(>100MB)失效,需要登录 > 20230824 123云盘解析大文件(>100MB)失效,需要登录
> 20230722 UC网盘解析失效需要登录 > 20230722 UC网盘解析失效需要登录

View File

@@ -1,2 +1,2 @@
app.version=${project.version} app.version=${project.version}
build=${build.timestamp} build=${maven.build.timestamp}

View File

@@ -37,6 +37,9 @@ public class WsTool extends PanBase {
MultiMap headers = MultiMap.caseInsensitiveMultiMap(); MultiMap headers = MultiMap.caseInsensitiveMultiMap();
headers.set("User-Agent", userAgent2); headers.set("User-Agent", userAgent2);
headers.set("Accept", "application/json, text/plain, */*");
headers.set("Content-Type", "application/json;charset=utf-8");
headers.set("Prod", "com.wenshushu.web.pc");
headers.set("sec-ch-ua-platform", "Android"); headers.set("sec-ch-ua-platform", "Android");
headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"); headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile"); headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
@@ -50,7 +53,7 @@ public class WsTool extends PanBase {
try { try {
// 设置匿名登录token // 设置匿名登录token
String token = asJson(res).getJsonObject("data").getString("token"); String token = asJson(res).getJsonObject("data").getString("token");
headers.set("X-Token", token); headers.set("X-TOKEN", token);
// 获取文件夹信息 // 获取文件夹信息
httpClient.postAbs(SHARE_URL_API + "task/mgrtask").putHeaders(headers) httpClient.postAbs(SHARE_URL_API + "task/mgrtask").putHeaders(headers)
@@ -68,10 +71,10 @@ public class WsTool extends PanBase {
String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid
// 调试输出文件夹信息 // 调试输出文件夹信息
System.out.println("文件夹期限: " + filetime); log.debug("文件夹期限: {}", filetime);
System.out.println("文件夹大小: " + filesize); log.debug("文件夹大小: {}", filesize);
System.out.println("文件夹pid: " + filepid); log.debug("文件夹pid: {}", filepid);
System.out.println("文件夹bid: " + filebid); log.debug("文件夹bid: {}", filebid);
// 获取文件信息 // 获取文件信息
httpClient.postAbs(SHARE_URL_API + "ufile/list").putHeaders(headers) httpClient.postAbs(SHARE_URL_API + "ufile/list").putHeaders(headers)
@@ -92,21 +95,21 @@ public class WsTool extends PanBase {
if (res3.statusCode() == 200) { if (res3.statusCode() == 200) {
try { try {
// 获取文件信息 // 获取文件信息
String filename = asJson(res3).getJsonObject("data") JsonObject fileInfo = asJson(res3).getJsonObject("data")
.getJsonArray("fileList").getJsonObject(0).getString("fname"); // 文件名称 .getJsonArray("fileList").getJsonObject(0);
String filefid = asJson(res3).getJsonObject("data") String filename = fileInfo.getString("fname"); // 文件名称
.getJsonArray("fileList").getJsonObject(0).getString("fid"); // 文件fid String fileUfileid = fileInfo.getString("ufileid", fileInfo.getString("fid")); // 文件ufileid
// 调试输出文件信息 // 调试输出文件信息
System.out.println("文件名称: " + filename); log.debug("文件名称: {}", filename);
System.out.println("文件fid: " + filefid); log.debug("文件ufileid: {}", fileUfileid);
// 检查文件是否失效 // 检查文件是否失效
httpClient.postAbs(SHARE_URL_API + "dl/sign").putHeaders(headers) httpClient.postAbs(SHARE_URL_API + "dl/sign").putHeaders(headers)
.sendJsonObject(JsonObject.of( .sendJsonObject(JsonObject.of(
"consumeCode", 0, "ufileid", fileUfileid,
"type", 1, // 新版接口不再需要consumeCode
"ufileid", filefid "type", 1
)).onSuccess(res4 -> { )).onSuccess(res4 -> {
if (res4.statusCode() == 200) { if (res4.statusCode() == 200) {
@@ -115,7 +118,7 @@ public class WsTool extends PanBase {
String fileurl = asJson(res4).getJsonObject("data").getString("url"); String fileurl = asJson(res4).getJsonObject("data").getString("url");
// 调试输出文件直链 // 调试输出文件直链
System.out.println("文件直链: " + fileurl); log.debug("文件直链: {}", fileurl);
if (!fileurl.equals("")) { if (!fileurl.equals("")) {
promise.complete(URLDecoder.decode(fileurl, StandardCharsets.UTF_8)); promise.complete(URLDecoder.decode(fileurl, StandardCharsets.UTF_8));

View File

@@ -11,7 +11,7 @@
## 测试配置 ## 测试配置
### 小飞机网盘 ✅ ### 小飞机网盘 ✅
- **用户名**: 15764091073 - **用户名**: 15x
- **URL**: https://share.feijipan.com/s/ZWYoZ31c - **URL**: https://share.feijipan.com/s/ZWYoZ31c
- **文件**: 资源.rar (1.13 GB) - **文件**: 资源.rar (1.13 GB)
- **认证方式**: username/password - **认证方式**: username/password
@@ -32,7 +32,7 @@
``` ```
=== 测试小飞机网盘解析(带认证)=== === 测试小飞机网盘解析(带认证)===
分享链接: https://share.feijipan.com/s/ZWYoZ31c 分享链接: https://share.feijipan.com/s/ZWYoZ31c
用户名: 15764091073 用户名: 15x
密码: ****** 密码: ******
开始解析... 开始解析...

BIN
web-front/img/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

View File

@@ -5,6 +5,15 @@ const axiosInstance = axios.create({
withCredentials: true // 重要允许跨域请求携带cookie withCredentials: true // 重要允许跨域请求携带cookie
}); });
// 请求拦截器将存储的Token添加到Authorization请求头
axiosInstance.interceptors.request.use(config => {
const token = localStorage.getItem('playground_token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
/** /**
* 演练场API服务 * 演练场API服务
*/ */
@@ -30,7 +39,12 @@ export const playgroundApi = {
async login(password) { async login(password) {
try { try {
const response = await axiosInstance.post('/v2/playground/login', { password }); const response = await axiosInstance.post('/v2/playground/login', { password });
return response.data; const data = response.data;
// 登录成功时从响应中提取并保存Token
if ((data.code === 200 || data.success) && data.data?.token) {
localStorage.setItem('playground_token', data.data.token);
}
return data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '登录失败'); throw new Error(error.response?.data?.error || error.message || '登录失败');
} }

View File

@@ -68,7 +68,7 @@
</div> </div>
<div class="demo-basic--circle"> <div class="demo-basic--circle">
<div class="block" style="text-align: center;"> <div class="block" style="text-align: center;">
<img :height="150" src="../../public/images/lanzou111.png" alt="lz"> <img :height="150" src="../../public/images/logo01.png" alt="lz">
</div> </div>
</div> </div>
<!-- 项目简介移到卡片内 --> <!-- 项目简介移到卡片内 -->

View File

@@ -1146,10 +1146,11 @@ function parseById(shareLinkInfo, http, logger) {
const isAuthed = res.data.authed || res.data.public; const isAuthed = res.data.authed || res.data.public;
authed.value = isAuthed; authed.value = isAuthed;
// 如果后端session已失效清除localStorage // 如果后端认证已失效清除localStorage中的认证信息
if (!isAuthed && savedAuth === 'true') { if (!isAuthed && savedAuth === 'true') {
localStorage.removeItem('playground_authed'); localStorage.removeItem('playground_authed');
localStorage.removeItem('playground_auth_time'); localStorage.removeItem('playground_auth_time');
localStorage.removeItem('playground_token');
} }
return isAuthed; return isAuthed;

View File

@@ -4,6 +4,10 @@ import io.vertx.core.json.JsonObject;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.security.SecureRandom;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* JS演练场配置 * JS演练场配置
* *
@@ -13,11 +17,21 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class PlaygroundConfig { public class PlaygroundConfig {
/** Token有效期24小时 */
private static final long TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000L;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/** /**
* 单例实例 * 单例实例
*/ */
private static PlaygroundConfig instance; private static PlaygroundConfig instance;
/**
* 已颁发的认证Token及其创建时间
*/
private final Map<String, Long> validTokens = new ConcurrentHashMap<>();
/** /**
* 是否启用演练场 * 是否启用演练场
* 默认false不启用 * 默认false不启用
@@ -42,6 +56,39 @@ public class PlaygroundConfig {
private PlaygroundConfig() { private PlaygroundConfig() {
} }
/**
* 生成并存储一个新的认证Token同时清理过期Token
*/
public String generateToken() {
// 清理过期Token防止Map无限增长
long now = System.currentTimeMillis();
validTokens.entrySet().removeIf(e -> now - e.getValue() > TOKEN_EXPIRY_MS);
// 使用SecureRandom生成32字节的密码学安全Token
byte[] bytes = new byte[32];
SECURE_RANDOM.nextBytes(bytes);
StringBuilder sb = new StringBuilder(64);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
String token = sb.toString();
validTokens.put(token, now);
return token;
}
/**
* 校验Token是否合法且未过期
*/
public boolean validateToken(String token) {
if (token == null || token.isEmpty()) return false;
Long createdAt = validTokens.get(token);
if (createdAt == null) return false;
if (System.currentTimeMillis() - createdAt > TOKEN_EXPIRY_MS) {
validTokens.remove(token);
return false;
}
return true;
}
/** /**
* 获取单例实例 * 获取单例实例
*/ */

View File

@@ -21,7 +21,6 @@ import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -47,7 +46,6 @@ public class PlaygroundApi {
private static final int MAX_PARSER_COUNT = 100; private static final int MAX_PARSER_COUNT = 100;
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制 private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/** /**
@@ -74,14 +72,14 @@ public class PlaygroundApi {
return true; return true;
} }
// 否则检查Session中的认证状态 // 检查 Authorization: Bearer <token> 请求头
Session session = ctx.session(); String authHeader = ctx.request().getHeader("Authorization");
if (session == null) { if (authHeader != null && authHeader.startsWith("Bearer ") && authHeader.length() > 7) {
return false; String token = authHeader.substring(7).trim();
return config.validateToken(token);
} }
Boolean authed = session.get(SESSION_AUTH_KEY); return false;
return authed != null && authed;
} }
/** /**
@@ -116,12 +114,8 @@ public class PlaygroundApi {
try { try {
PlaygroundConfig config = PlaygroundConfig.getInstance(); PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接成功 // 如果是公开模式,直接成功不需要token
if (config.isPublic()) { if (config.isPublic()) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject()); promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
return promise.future(); return promise.future();
} }
@@ -137,11 +131,9 @@ public class PlaygroundApi {
// 验证密码 // 验证密码
if (config.getPassword().equals(password)) { if (config.getPassword().equals(password)) {
Session session = ctx.session(); String token = config.generateToken();
if (session != null) { JsonObject tokenData = new JsonObject().put("token", token);
session.put(SESSION_AUTH_KEY, true); promise.complete(JsonResult.data(tokenData).toJsonObject());
}
promise.complete(JsonResult.success("登录成功").toJsonObject());
} else { } else {
promise.complete(JsonResult.error("密码错误").toJsonObject()); promise.complete(JsonResult.error("密码错误").toJsonObject());
} }