fix(security): 安全漏洞修复与依赖升级

- 升级 Vert.x 4.5.24 → 4.5.27, postgresql 42.7.3 → 42.7.11, logback 1.5.18 → 1.5.32, axios 1.13.5 → 1.16.1
- 修复 JWT 签名验证和密码比较的时序攻击漏洞 (MessageDigest.isEqual)
- 修复 AESUtils 使用不安全 Random 改为 SecureRandom
- 修复登录用户枚举和异常信息泄露,统一错误提示
- 修复 RateLimiter count++ 非原子操作 (AtomicInteger)
- 修复 JsParserExecutor DCL 模式缺少 volatile
- 修复 Token 日志泄露,仅打印前8字符
- 修复 Playground 密码时序攻击和堆栈泄露
- 所有 window.open 添加 noopener,noreferrer
- LocalConstant 改用 ConcurrentHashMap 保证线程安全
- Dockerfile 添加非 root 用户运行,secret.yml 加入 .gitignore
This commit is contained in:
yukaidi
2026-05-29 14:20:54 +08:00
parent ff400d3be3
commit 17460ff271
22 changed files with 212 additions and 155 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@ target/
sdkTest.log
app.yml
app-local.yml
secret.yml
#some local files

View File

@@ -10,8 +10,13 @@ COPY ./web-service/target/netdisk-fast-download-bin.zip .
RUN unzip netdisk-fast-download-bin.zip && \
mv netdisk-fast-download/* ./ && \
rm netdisk-fast-download-bin.zip && \
chmod +x run.sh
chmod +x run.sh && \
mkdir -p db logs
EXPOSE 6400 6401
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["sh", "run.sh"]
EXPOSE 6401
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -65,7 +65,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<version>42.7.11</version>
</dependency>
</dependencies>

View File

@@ -127,8 +127,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 错误请求处理
mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult
.error("Method Not Allowed", 405)));
mainRouter.errorHandler(404, ctx -> ctx.response().setStatusCode(404).setChunked(true)
.end("Internal server error: 404 not found"));
mainRouter.errorHandler(404, ctx -> {
ctx.response().setStatusCode(404).end("404 not found");
});
return mainRouter;
}
@@ -179,8 +180,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
if (ctx.statusCode() == 503 || ctx.failure() == null) {
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
} else {
ctx.failure().printStackTrace();
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500);
LOGGER.error("路由处理失败", ctx.failure());
String msg = ctx.failure() != null ? ctx.failure().getMessage() : "未知异常";
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
}
});
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
@@ -198,7 +200,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
try {
ReflectionUtil.invokeWithArguments(method, instance, sock);
} catch (Throwable e) {
e.printStackTrace();
LOGGER.error("WebSocket处理异常", e);
}
});
if (url.endsWith("*")) {
@@ -322,7 +324,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
parameterValueList.put(k, entity);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
LOGGER.error("实体类绑定异常: {}", typeName, e);
}
}
});
@@ -365,7 +367,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
Object entity = ParamUtil.multiMapToEntity(queryParams, aClass);
parameterValueList.put(k, entity);
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("参数绑定异常: {}", v.getRight().getName(), e);
}
} else if (parameterValueList.get(k) == null
&& JsonObject.class.getName().equals(v.getRight().getName())) {
@@ -408,22 +410,19 @@ public class RouterHandlerFactory implements BaseHttpApi {
doFireJsonResultResponse(ctx, JsonResult.data(null));
}
}).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()), 500));
}).onFailure(e -> {
LOGGER.error("请求处理失败", e);
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
});
} else {
doFireJsonResultResponse(ctx, JsonResult.data(data));
}
}
} catch (Throwable e) {
e.printStackTrace();
String err = e.getMessage();
if (e.getCause() != null) {
if (e.getCause() instanceof InvocationTargetException) {
err = ((InvocationTargetException) e.getCause()).getTargetException().getMessage();
} else {
err = e.getCause().getMessage();
}
}
doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
LOGGER.error("请求处理异常", e);
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
}
}

View File

@@ -1,7 +1,7 @@
package cn.qaiu.vx.core.util;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* vertx 上下文外的本地容器 为不在vertx线程的方法传递数据
@@ -10,11 +10,10 @@ import java.util.Map;
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class LocalConstant {
private static final Map<String, Object> LOCAL_CONST = new HashMap<>();
private static final Map<String, Object> LOCAL_CONST = new ConcurrentHashMap<>();
public static Map<String, Object> put(String k, Object v) {
if (LOCAL_CONST.containsKey(k)) return LOCAL_CONST;
LOCAL_CONST.put(k, v);
LOCAL_CONST.putIfAbsent(k, v);
return LOCAL_CONST;
}

9
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
# Fix permissions on volume-mounted directories (runs as root)
chown -R appuser:appgroup /app/db /app/logs /app/resources 2>/dev/null || true
# Run Java directly - entrypoint is PID 1, exec makes Java PID 1
# Docker SIGTERM goes directly to Java, triggering ShutdownHook
exec java -Xmx${JVM_XMX:-512M} ${JVM_OPTS} -Duser.timezone=${TZ:-Asia/Shanghai} -jar /app/netdisk-fast-download.jar

View File

@@ -12,7 +12,7 @@
<groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId>
<version>10.2.5</version>
<version>${parserVersion}</version>
<packaging>jar</packaging>
<name>cn.qaiu:parser</name>
@@ -35,9 +35,9 @@
</developers>
<scm>
<connection>scm:git:https://github.com/qaiu/netdisk-fast-download.git</connection>
<developerConnection>scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
<url>https://github.com/qaiu/netdisk-fast-download</url>
<connection>scm:git:https://github.com/${github.owner}/${github.repo}.git</connection>
<developerConnection>scm:git:ssh://git@github.com:${github.owner}/${github.repo}.git</developerConnection>
<url>https://github.com/${github.owner}/${github.repo}</url>
</scm>
<distributionManagement>
@@ -52,20 +52,19 @@
</distributionManagement>
<properties>
<revision>0.2.1</revision>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Versions -->
<vertx.version>4.5.24</vertx.version>
<vertx.version>4.5.27</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<jackson.version>2.18.6</jackson.version>
<logback.version>1.5.19</logback.version>
<logback.version>1.5.32</logback.version>
<junit.version>4.13.2</junit.version>
</properties>
@@ -124,6 +123,41 @@
<build>
<plugins>
<!-- 从 git remote origin 自动识别 GitHub 仓库地址 -->
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>4.1.1</version>
<dependencies>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.24</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>initialize</phase>
<goals><goal>execute</goal></goals>
<configuration>
<scripts>
<script>
def url = 'git remote get-url origin'.execute().text.trim()
def m = (url =~ 'github\\.com[:/]([^/]+)/([^/.]+?)(?:\\.git)?$')
if (m.find()) {
project.properties.setProperty('github.owner', m.group(1))
project.properties.setProperty('github.repo', m.group(2))
} else {
project.properties.setProperty('github.owner', 'qaiu')
project.properties.setProperty('github.repo', 'netdisk-fast-download')
}
</script>
</scripts>
</configuration>
</execution>
</executions>
</plugin>
<!-- 编译 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@@ -33,7 +33,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
private static WorkerExecutor EXECUTOR;
private static volatile WorkerExecutor EXECUTOR;
private static final Object EXECUTOR_LOCK = new Object();
private static String FETCH_RUNTIME_JS = null;

View File

@@ -14,7 +14,6 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.HexFormat;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -299,7 +298,7 @@ public class AESUtils {
//length用户要求产生字符串的长度
public static String getRandomString(int length){
String str="abcdefghijklmnopqrstuvwxyz0123456789";
Random random=new Random();
SecureRandom random=new SecureRandom();
StringBuilder sb=new StringBuilder();
for(int i=0;i<length;i++){
int number=random.nextInt(36);

View File

@@ -25,16 +25,17 @@
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
<!-- Vert.x 4.5.24 已包含安全修复,无需单独指定 Netty 版本 -->
<vertx.version>4.5.24</vertx.version>
<!-- Vert.x 4.5.27 包含安全修复,无需单独指定 Netty 版本 -->
<vertx.version>4.5.27</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
<parserVersion>10.2.5</parserVersion>
<jackson.version>2.18.6</jackson.version>
<!-- Logback 最新稳定版 -->
<logback.version>1.5.18</logback.version>
<logback.version>1.5.32</logback.version>
<junit.version>4.13.2</junit.version>
</properties>
@@ -74,7 +75,7 @@
<dependency>
<groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId>
<version>10.2.5</version>
<version>${parserVersion}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -5,15 +5,15 @@
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build && node scripts/compress-vs.js",
"build:no-compress": "vue-cli-service build",
"build": "node scripts/sync-version.js && vue-cli-service build && node scripts/compress-vs.js",
"build:no-compress": "node scripts/sync-version.js && vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0",
"@vueuse/core": "^11.2.0",
"axios": "1.13.5",
"axios": "1.16.1",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",

View File

@@ -36,7 +36,6 @@ if (item) {
const darkMode = ref(item)
watch(darkMode, (newValue) => {
console.log(`darkMode: ${newValue}`)
window.localStorage.setItem("darkMode", newValue);
// 发射主题变化事件

View File

@@ -293,24 +293,6 @@ export default {
return clientConfig[type]?.downloadUrl || '#'
}
// 判断是否应该显示下载客户端按钮
const shouldShowDownloadButton = (type) => {
const os = getOSInfo()
switch (type) {
case 'CURL':
// cURL 在 Windows 上可能需要安装
return os === 'windows'
case 'ARIA2':
// Aria2 需要手动安装
return true
case 'THUNDER':
// 迅雷主要在 Windows 上使用
return os === 'windows'
default:
return false
}
}
// 获取操作系统信息
const getOSInfo = () => {
const userAgent = navigator.userAgent.toLowerCase()
@@ -369,7 +351,7 @@ export default {
copyToClipboard(link)
return
}
window.open(link, '_blank')
window.open(link, '_blank', 'noopener,noreferrer')
ElMessage.success('正在唤起迅雷下载')
break
@@ -383,13 +365,6 @@ export default {
}
}
// 下载客户端
const downloadClient = (type) => {
const url = getClientDownloadUrl(type)
window.open(url, '_blank')
ElMessage.success(`正在跳转到 ${getClientDisplayName(type)} 下载页面`)
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes) return '未知'
@@ -440,9 +415,7 @@ export default {
getTextareaRows,
goBack,
getClientLogo,
downloadClient,
handleImageError,
shouldShowDownloadButton,
getClientSupportsCookie,
goToAuthConfig
}

View File

@@ -19,11 +19,11 @@
</el-dialog> -->
<!-- 顶部反馈栏小号灰色无红边框 -->
<div class="feedback-bar">
<a href="https://github.com/qaiu/netdisk-fast-download/issues" target="_blank" rel="noopener" class="feedback-link mini">
<a :href="githubRepoUrl + '/issues'" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-bug feedback-icon"></i>
反馈
</a>
<a href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini">
<a :href="githubRepoUrl" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fab fa-github feedback-icon"></i>
源码
</a>
@@ -73,9 +73,9 @@
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.3.0</div>
<div class="intro-title">NFD网盘直链解析 {{ projectVersion }}</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘iCloud移动云空间联想乐云QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘iCloud移动云空间联想乐云QQ闪传等 <el-link style="color:#606cf5" :href="githubRepoUrl + '?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5'" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div>
</div>
@@ -90,7 +90,7 @@
<!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url" @paste="onPaste">
<template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard">
<el-button @click="getPaste(true)">读取剪切板</el-button>
@@ -595,11 +595,10 @@ import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi'
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
import { PREVIEW_BASE_URL } from '@/utils/constants'
export default {
name: 'App',
name: 'Home',
components: { DarkMode, DirectoryTree, DownloadDialog },
mixins: [fileTypeUtils],
data() {
@@ -617,7 +616,7 @@ export default {
parseResult: {},
downloadUrl: null,
directLink: '',
previewBaseUrl,
previewBaseUrl: PREVIEW_BASE_URL,
// 功能结果
markdownText: '',
@@ -714,6 +713,12 @@ export default {
}
},
computed: {
githubRepoUrl() {
return process.env.VUE_APP_GITHUB_REPO_URL
},
projectVersion() {
return process.env.VUE_APP_VERSION || '0.0.0'
},
// 检查是否配置了认证信息(针对当前链接的网盘类型)
hasAuthConfig() {
const panType = this.getCurrentPanType()
@@ -959,18 +964,16 @@ export default {
// 优先使用个人配置
if (this.allAuthConfigs[panType]) {
config = this.allAuthConfigs[panType]
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
} else {
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth
try {
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
const encryptedAuth = response.data?.data?.encryptedAuth
if (encryptedAuth) {
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
return encryptedAuth
}
} catch (e) {
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
// no available donated account
}
return ''
}
@@ -1091,17 +1094,45 @@ export default {
}
},
// 识别并转换短链输入(如 lz:shareKey@pwd
// 识别并转换短链输入(如 lz:shareKey@pwd,或从文本中提取链接
normalizeShortcutInput() {
const shortInfo = this.expandShortFormat(this.link)
if (!shortInfo) return
if (!this.link) return
const trimmed = this.link.trim()
if (!trimmed) return
this.link = shortInfo.link
if (!this.password && shortInfo.pwd) {
this.password = shortInfo.pwd
// 已经是直接链接,跳过
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return
// 尝试短格式
const shortInfo = this.expandShortFormat(trimmed)
if (shortInfo) {
this.link = shortInfo.link
if (!this.password && shortInfo.pwd) {
this.password = shortInfo.pwd
}
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
return
}
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
// 从文本中自动提取链接
const linkInfo = parserUrl.parseLink(trimmed)
if (linkInfo.link) {
this.link = linkInfo.link
const pwd = parserUrl.parsePwd(trimmed)
if (!this.password && pwd) {
this.password = pwd
}
this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`)
this.updateDirectLink()
}
},
// 粘贴事件:从粘贴的文本中自动提取链接
onPaste(e) {
this.$nextTick(() => {
this.normalizeShortcutInput()
})
},
// 清除结果
@@ -1126,17 +1157,24 @@ export default {
params.auth = authParam
}
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
if (response.data.code === 200) {
// this.$message.success(response.data.msg || '操作成功')
return response.data
} else {
// 在页面右下角显示一个查看详情”按钮 可以查看原json
// 在页面右下角显示一个查看详情”按钮 可以查看原json
this.errorDetail = response?.data
this.errorButtonVisible = true
throw new Error(response.data.msg || '操作失败')
}
} catch (error) {
// HTTP 非2xx时从响应体中提取后端返回的错误信息
if (error.response?.data?.msg) {
this.errorDetail = error.response.data
this.errorButtonVisible = true
this.$message.error(error.response.data.msg)
throw new Error(error.response.data.msg)
}
this.$message.error(error.message || '网络错误')
throw error
} finally {
@@ -1309,7 +1347,7 @@ export default {
// 文件点击处理
handleFileClick(file) {
if (file.parserUrl) {
window.open(file.parserUrl, '_blank')
window.open(file.parserUrl, '_blank', 'noopener,noreferrer')
} else {
this.$message.warning('该文件暂无下载链接')
}
@@ -1319,7 +1357,6 @@ export default {
async getPaste(isManual = false) {
try {
const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text)
const shortInfo = this.expandShortFormat(text)
if (shortInfo) {
@@ -1364,7 +1401,9 @@ export default {
}
} catch (error) {
console.error('读取剪切板失败:', error)
this.$message.error('读取剪切板失败,请检查浏览器权限')
if (isManual) {
this.$message.warning('读取剪切板失败,请手动粘贴链接到输入框')
}
}
},
@@ -1439,7 +1478,7 @@ export default {
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制分享信息和错误详情');
window.open('https://github.com/qaiu/netdisk-fast-download/issues/new', '_blank');
window.open(`${this.githubRepoUrl}/issues/new`, '_blank', 'noopener,noreferrer');
}).catch(() => {
this.$message.error('复制失败');
});
@@ -1786,19 +1825,26 @@ export default {
}
// 监听窗口焦点事件
window.addEventListener('focus', () => {
this._onFocusHandler = () => {
if (this.autoReadClipboard) {
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
this.getPaste()
}
})
}
window.addEventListener('focus', this._onFocusHandler)
// 首次打开页面弹出风险提示
if (!window.localStorage.getItem('nfd_risk_ack')) {
this.showRiskDialog = true
}
},
beforeUnmount() {
if (this._onFocusHandler) {
window.removeEventListener('focus', this._onFocusHandler)
}
},
watch: {
downloadUrl(val) {
if (!val) {

View File

@@ -653,22 +653,22 @@
<p>更多详细信息请参考 GitHub 仓库文档</p>
<ul>
<li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer">
<a :href="githubRepoUrl + '/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
JavaScript 解析器开发指南
</a>
</li>
<li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer">
<a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
自定义解析器扩展指南
</a>
</li>
<li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md" target="_blank" rel="noopener noreferrer">
<a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md'" target="_blank" rel="noopener noreferrer">
快速开始教程
</a>
</li>
<li>
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/README.md" target="_blank" rel="noopener noreferrer">
<a :href="githubRepoUrl + '/blob/main/parser/README.md'" target="_blank" rel="noopener noreferrer">
解析器模块文档
</a>
</li>
@@ -858,6 +858,7 @@ export default {
},
setup() {
const router = useRouter();
const githubRepoUrl = process.env.VUE_APP_GITHUB_REPO_URL;
// 语言常量
const LANGUAGE = {
@@ -1178,7 +1179,7 @@ function parseById(shareLinkInfo, http, logger) {
// 新窗口打开首页
const goHomeInNewWindow = () => {
window.open('/', '_blank');
window.open('/', '_blank', 'noopener,noreferrer');
};
// 检查是否有未保存的文件
@@ -1758,7 +1759,6 @@ function parseFileList(shareLinkInfo, http, logger) {
testParams.value.method
);
console.log('测试结果:', result);
testResult.value = result;
// 将日志添加到控制台
@@ -1820,10 +1820,8 @@ function parseFileList(shareLinkInfo, http, logger) {
loadingList.value = true;
try {
const result = await playgroundApi.getParserList();
console.log('获取解析器列表响应:', result);
// 检查响应格式
if (result.code === 200 || result.success) {
console.log('列表数据:', result.data);
parserList.value = result.data || [];
} else if (result.data && Array.isArray(result.data)) {
// 如果data直接是数组
@@ -1857,7 +1855,6 @@ function parseFileList(shareLinkInfo, http, logger) {
try {
const codeToPublish = currentCode.value;
const result = await playgroundApi.saveParser(codeToPublish);
console.log('保存解析器响应:', result);
// 检查响应格式
if (result.code === 200 || result.success) {
// 从响应或代码中提取type信息
@@ -2223,6 +2220,8 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
}, 100);
};
let themeObserver = null;
onMounted(async () => {
// 初始化移动端检测
updateIsMobile();
@@ -2249,10 +2248,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
const html = document.documentElement;
if (html && html.classList) {
try {
const observer = new MutationObserver(() => {
themeObserver = new MutationObserver(() => {
checkDarkMode();
});
observer.observe(html, {
themeObserver.observe(html, {
attributes: true,
attributeFilter: ['class', 'data-theme']
});
@@ -2269,9 +2268,11 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
window.removeEventListener('resize', updateIsMobile);
// 移除页面关闭/刷新前的提示
window.removeEventListener('beforeunload', handleBeforeUnload);
themeObserver?.disconnect();
});
return {
githubRepoUrl,
LANGUAGE,
editorRef,
jsCode,

View File

@@ -32,7 +32,7 @@
<script>
import axios from 'axios'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { previewBaseUrl } from '@/views/Home.vue'
import { PREVIEW_BASE_URL } from '@/utils/constants'
export default {
name: 'ShowFile',
@@ -44,7 +44,7 @@ export default {
downloadUrl: '',
shareUrl: '', // 添加原始分享链接
fileTypeUtils,
previewBaseUrl
previewBaseUrl: PREVIEW_BASE_URL
}
},
methods: {
@@ -73,7 +73,7 @@ export default {
this.parseResult = res.data
this.downloadUrl = res.data.data?.directLink
} catch (e) {
this.error = '解析失败'
this.error = e.response?.data?.msg || e.response?.data?.error || '解析失败'
} finally {
this.loading = false
}

View File

@@ -55,7 +55,7 @@ export default {
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
this.directoryData = res.data.data || []
} catch (e) {
this.error = '目录解析失败'
this.error = e.response?.data?.msg || e.response?.data?.error || '目录解析失败'
} finally {
this.loading = false
}

View File

@@ -10,6 +10,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class RateLimiter {
@@ -51,12 +52,12 @@ public class RateLimiter {
return new RequestInfo(1, currentTime);
} else {
// 增加计数器
requestInfo.count++;
requestInfo.count.incrementAndGet();
return requestInfo;
}
});
if (info.count > MAX_REQUESTS) {
if (info.count.get() > MAX_REQUESTS) {
// 超过限制
// 计算剩余时间
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
@@ -71,11 +72,11 @@ public class RateLimiter {
}
private static class RequestInfo {
volatile int count;
final AtomicInteger count;
volatile long timestamp;
RequestInfo(int count, long time) {
this.count = count;
this.count = new AtomicInteger(count);
this.timestamp = time;
}
}

View File

@@ -7,6 +7,7 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -93,9 +94,10 @@ public class JwtUtil {
String encodedPayload = parts[1];
String signature = parts[2];
// 验证签名
// 验证签名(使用常量时间比较防止时序攻击)
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
if (!expectedSignature.equals(signature)) {
if (!MessageDigest.isEqual(expectedSignature.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return false;
}

View File

@@ -80,8 +80,10 @@ public class PasswordUtil {
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
// 比较计算出的哈希值和存储的哈希值
return hashBase64.equals(calculatedHashBase64);
// 比较计算出的哈希值和存储的哈希值(使用常量时间比较防止时序攻击)
return MessageDigest.isEqual(
hashBase64.getBytes(StandardCharsets.UTF_8),
calculatedHashBase64.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
// 如果发生异常例如格式不正确返回false
return false;

View File

@@ -28,6 +28,7 @@ import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@@ -129,8 +130,11 @@ public class PlaygroundApi {
return promise.future();
}
// 验证密码
if (config.getPassword().equals(password)) {
// 验证密码(使用常量时间比较防止时序攻击)
String storedPassword = config.getPassword();
if (storedPassword != null && MessageDigest.isEqual(
storedPassword.getBytes(StandardCharsets.UTF_8),
password.getBytes(StandardCharsets.UTF_8))) {
String token = config.generateToken();
JsonObject tokenData = new JsonObject().put("token", token);
promise.complete(JsonResult.data(tokenData).toJsonObject());
@@ -299,7 +303,6 @@ public class PlaygroundApi {
}).onFailure(e -> {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场执行失败", e);
@@ -317,7 +320,6 @@ public class PlaygroundApi {
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(respLogs)
.build();
@@ -328,14 +330,12 @@ public class PlaygroundApi {
} catch (Exception e) {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场初始化失败", e);
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(new ArrayList<>())
.build();
@@ -346,8 +346,7 @@ public class PlaygroundApi {
log.error("解析请求参数失败", e);
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("解析请求参数失败: " + e.getMessage())
.stackTrace(getStackTrace(e))
.error("解析请求参数失败")
.build()));
}
@@ -696,18 +695,5 @@ public class PlaygroundApi {
}
return ip;
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Throwable throwable) {
if (throwable == null) {
return "";
}
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
}

View File

@@ -125,18 +125,18 @@ public class UserServiceImpl implements UserService {
if (rows.size() == 0) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
.put("message", "用户名或密码错误"));
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
// 验证密码
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "密码错误"));
.put("message", "用户名或密码错误"));
return;
}
@@ -169,7 +169,7 @@ public class UserServiceImpl implements UserService {
log.error("登录查询失败", err);
promise.complete(new JsonObject()
.put("success", false)
.put("message", "登录失败: " + err.getMessage()));
.put("message", "登录失败,请稍后重试"));
});
return promise.future();
@@ -189,10 +189,10 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(username))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
promise.fail("用户名或密码错误");
return;
}
Row row = rows.iterator().next();
SysUser user = rowToUser(row);
promise.complete(filterSensitiveInfo(user));
@@ -296,10 +296,10 @@ public class UserServiceImpl implements UserService {
.execute(Tuple.of(user.getUsername()))
.onSuccess(rows -> {
if (rows.size() == 0) {
promise.fail("用户不存在");
promise.fail("用户名或密码错误");
return;
}
Row row = rows.iterator().next();
SysUser existUser = rowToUser(row);
@@ -406,7 +406,7 @@ public class UserServiceImpl implements UserService {
.onFailure(err -> {
promise.complete(new JsonObject()
.put("success", false)
.put("message", "用户不存在"));
.put("message", "认证失败,请重新登录"));
});
return promise.future();