mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-02-24 14:15:24 +00:00
feat(v0.2.1): 添加认证参数支持和客户端下载命令生成
主要更新: - 新增 auth 参数加密传递支持 (QK/UC Cookie认证) - 实现下载命令自动生成 (curl/aria2c/迅雷) - aria2c 命令支持 8 线程 8 片段下载 - 修复 cookie 字段映射问题 - 优化前端 clientLinks 页面 - 添加认证参数文档和测试用例 - 更新 .gitignore 忽略编译目录
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
package cn.qaiu.lz.common.util;
|
||||
|
||||
import cn.qaiu.lz.web.model.AuthParam;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import cn.qaiu.vx.core.util.SharedDataUtil;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 认证参数编解码工具类
|
||||
* <p>
|
||||
* 编码流程: JSON -> AES加密 -> Base64编码 -> URL编码
|
||||
* 解码流程: URL解码 -> Base64解码 -> AES解密 -> JSON解析
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2026/2/5
|
||||
*/
|
||||
@Slf4j
|
||||
public class AuthParamCodec {
|
||||
|
||||
/**
|
||||
* 默认加密密钥(16位)
|
||||
*/
|
||||
private static final String DEFAULT_ENCRYPT_KEY = "nfd_auth_key2026";
|
||||
|
||||
/**
|
||||
* 配置中的密钥路径
|
||||
*/
|
||||
private static final String CONFIG_KEY_PATH = "authEncryptKey";
|
||||
|
||||
private AuthParamCodec() {
|
||||
// 工具类禁止实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密密钥
|
||||
* 优先从配置文件读取,如果未配置则使用默认密钥
|
||||
*/
|
||||
public static String getEncryptKey() {
|
||||
try {
|
||||
String configKey = SharedDataUtil.getJsonStringForServerConfig(CONFIG_KEY_PATH);
|
||||
if (StringUtils.isNotBlank(configKey)) {
|
||||
return configKey;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("从配置读取加密密钥失败,使用默认密钥: {}", e.getMessage());
|
||||
}
|
||||
return DEFAULT_ENCRYPT_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码认证参数
|
||||
* 解码流程: URL解码 -> Base64解码 -> AES解密 -> JSON解析
|
||||
*
|
||||
* @param encryptedAuth 加密后的认证参数字符串
|
||||
* @return AuthParam 对象,解码失败返回 null
|
||||
*/
|
||||
public static AuthParam decode(String encryptedAuth) {
|
||||
return decode(encryptedAuth, getEncryptKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码认证参数(指定密钥)
|
||||
*
|
||||
* @param encryptedAuth 加密后的认证参数字符串
|
||||
* @param key AES密钥(16位)
|
||||
* @return AuthParam 对象,解码失败返回 null
|
||||
*/
|
||||
public static AuthParam decode(String encryptedAuth, String key) {
|
||||
if (StringUtils.isBlank(encryptedAuth)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: URL解码
|
||||
String urlDecoded = URLDecoder.decode(encryptedAuth, StandardCharsets.UTF_8);
|
||||
log.debug("URL解码结果: {}", urlDecoded);
|
||||
|
||||
// Step 2: Base64解码
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
log.debug("Base64解码成功,长度: {}", base64Decoded.length);
|
||||
|
||||
// Step 3: AES解密
|
||||
String jsonStr = AESUtils.decryptByAES(base64Decoded, key);
|
||||
log.debug("AES解密结果: {}", jsonStr);
|
||||
|
||||
// Step 4: JSON解析
|
||||
JsonObject json = new JsonObject(jsonStr);
|
||||
AuthParam authParam = new AuthParam(json);
|
||||
log.info("认证参数解码成功: authType={}", authParam.getAuthType());
|
||||
return authParam;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("认证参数Base64解码失败: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("认证参数解码失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码认证参数
|
||||
* 编码流程: JSON -> AES加密 -> Base64编码 -> URL编码
|
||||
*
|
||||
* @param authParam 认证参数对象
|
||||
* @return 加密后的字符串,编码失败返回 null
|
||||
*/
|
||||
public static String encode(AuthParam authParam) {
|
||||
return encode(authParam, getEncryptKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码认证参数(指定密钥)
|
||||
*
|
||||
* @param authParam 认证参数对象
|
||||
* @param key AES密钥(16位)
|
||||
* @return 加密后的字符串,编码失败返回 null
|
||||
*/
|
||||
public static String encode(AuthParam authParam, String key) {
|
||||
if (authParam == null || !authParam.hasValidAuth()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: 转换为JSON
|
||||
String jsonStr = authParam.toJsonObject().encode();
|
||||
log.debug("JSON字符串: {}", jsonStr);
|
||||
|
||||
// Step 2: AES加密 + Base64编码
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, key);
|
||||
log.debug("AES+Base64编码结果: {}", base64Encoded);
|
||||
|
||||
// Step 3: URL编码
|
||||
String urlEncoded = java.net.URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
log.debug("URL编码结果: {}", urlEncoded);
|
||||
|
||||
return urlEncoded;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("认证参数编码失败: {}", e.getMessage(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码认证参数(从JsonObject)
|
||||
*
|
||||
* @param json 认证参数JSON对象
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encode(JsonObject json) {
|
||||
return encode(new AuthParam(json));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码认证参数(从JSON字符串)
|
||||
*
|
||||
* @param jsonStr 认证参数JSON字符串
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encodeFromJsonString(String jsonStr) {
|
||||
if (StringUtils.isBlank(jsonStr)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonObject json = new JsonObject(jsonStr);
|
||||
return encode(new AuthParam(json));
|
||||
} catch (Exception e) {
|
||||
log.error("JSON解析失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证加密的认证参数是否有效
|
||||
*
|
||||
* @param encryptedAuth 加密后的认证参数
|
||||
* @return true 如果可以成功解码
|
||||
*/
|
||||
public static boolean isValid(String encryptedAuth) {
|
||||
return decode(encryptedAuth) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构建并编码认证参数
|
||||
*
|
||||
* @param authType 认证类型
|
||||
* @param token token/cookie/credential
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String quickEncode(String authType, String token) {
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType(authType)
|
||||
.token(token)
|
||||
.build();
|
||||
return encode(authParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速构建并编码用户名密码认证
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String quickEncodePassword(String username, String password) {
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType("password")
|
||||
.username(username)
|
||||
.password(password)
|
||||
.build();
|
||||
return encode(authParam);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ 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.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -18,6 +20,7 @@ import java.nio.charset.StandardCharsets;
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2024/9/13
|
||||
*/
|
||||
@Slf4j
|
||||
public class URLParamUtil {
|
||||
|
||||
/**
|
||||
@@ -60,12 +63,14 @@ public class URLParamUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接被截断的URL参数,忽略pwd参数
|
||||
// 拼接被截断的URL参数,忽略pwd、auth等参数
|
||||
StringBuilder urlBuilder = new StringBuilder(decodedUrl);
|
||||
boolean firstParam = !decodedUrl.contains("?");
|
||||
|
||||
for (String paramName : params.names()) {
|
||||
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId") && !paramName.equals("uuid")) { // 忽略 "url" 和 "pwd" 参数
|
||||
// 忽略 "url", "pwd", "dirId", "uuid", "auth" 参数(这些参数单独处理,不应拼接到分享URL中)
|
||||
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId")
|
||||
&& !paramName.equals("uuid") && !paramName.equals("auth")) {
|
||||
if (firstParam) {
|
||||
urlBuilder.append("?");
|
||||
firstParam = false;
|
||||
@@ -116,4 +121,152 @@ public class URLParamUtil {
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加临时认证参数(一次性,不保存到数据库或共享内存)
|
||||
* 如果提供了临时认证参数,将覆盖后台配置的认证信息
|
||||
*
|
||||
* @param parserCreate ParserCreate对象
|
||||
* @param authType 认证类型
|
||||
* @param authToken 认证token/用户名/accesstoken/cookie
|
||||
* @param authPassword 密码(仅用于username_password认证)
|
||||
* @param authInfo1-5 扩展认证信息(用于custom认证)
|
||||
*/
|
||||
public static void addTempAuthParam(ParserCreate parserCreate, String authType,
|
||||
String authToken, String authPassword,
|
||||
String authInfo1, String authInfo2, String authInfo3,
|
||||
String authInfo4, String authInfo5) {
|
||||
if (StringUtils.isBlank(authType) && StringUtils.isBlank(authToken)) {
|
||||
// 没有提供临时认证参数,使用后台配置
|
||||
addParam(parserCreate);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先添加代理配置和域名配置
|
||||
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
|
||||
.getLocalMap(ConfigConstant.LOCAL);
|
||||
String type = parserCreate.getShareLinkInfo().getType();
|
||||
|
||||
if (localMap.containsKey(ConfigConstant.PROXY)) {
|
||||
JsonObject proxy = (JsonObject) localMap.get(ConfigConstant.PROXY);
|
||||
if (proxy.containsKey(type)) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.PROXY, proxy.getJsonObject(type));
|
||||
}
|
||||
}
|
||||
|
||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||
|
||||
// 构建临时认证信息
|
||||
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();
|
||||
|
||||
if (StringUtils.isNotBlank(authType)) {
|
||||
tempAuth.set("authType", authType.trim());
|
||||
}
|
||||
|
||||
String authTypeValue = authType != null ? authType : "";
|
||||
switch (authTypeValue.toLowerCase()) {
|
||||
case "accesstoken":
|
||||
case "authorization":
|
||||
if (StringUtils.isNotBlank(authToken)) {
|
||||
tempAuth.set("token", authToken.trim());
|
||||
}
|
||||
break;
|
||||
|
||||
case "cookie":
|
||||
// cookie 类型需要同时设置 token 和 cookie 字段
|
||||
// QkTool/UcTool 等从 auths.get("cookie") 获取 cookie 值
|
||||
if (StringUtils.isNotBlank(authToken)) {
|
||||
tempAuth.set("token", authToken.trim());
|
||||
tempAuth.set("cookie", authToken.trim());
|
||||
}
|
||||
break;
|
||||
|
||||
case "password":
|
||||
case "username_password":
|
||||
if (StringUtils.isNotBlank(authToken)) {
|
||||
tempAuth.set("username", authToken.trim());
|
||||
tempAuth.set("token", authToken.trim()); // 兼容旧的解析器
|
||||
}
|
||||
if (StringUtils.isNotBlank(authPassword)) {
|
||||
tempAuth.set("password", authPassword.trim());
|
||||
}
|
||||
break;
|
||||
|
||||
case "custom":
|
||||
// 自定义认证支持多个扩展字段
|
||||
if (StringUtils.isNotBlank(authToken)) {
|
||||
tempAuth.set("token", authToken.trim());
|
||||
}
|
||||
if (StringUtils.isNotBlank(authInfo1)) {
|
||||
parseAndSetAuthInfo(tempAuth, authInfo1);
|
||||
}
|
||||
if (StringUtils.isNotBlank(authInfo2)) {
|
||||
parseAndSetAuthInfo(tempAuth, authInfo2);
|
||||
}
|
||||
if (StringUtils.isNotBlank(authInfo3)) {
|
||||
parseAndSetAuthInfo(tempAuth, authInfo3);
|
||||
}
|
||||
if (StringUtils.isNotBlank(authInfo4)) {
|
||||
parseAndSetAuthInfo(tempAuth, authInfo4);
|
||||
}
|
||||
if (StringUtils.isNotBlank(authInfo5)) {
|
||||
parseAndSetAuthInfo(tempAuth, authInfo5);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 默认处理:将authToken作为token
|
||||
if (StringUtils.isNotBlank(authToken)) {
|
||||
tempAuth.set("token", authToken.trim());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 设置临时认证信息(覆盖后台配置)
|
||||
if (!tempAuth.isEmpty()) {
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, tempAuth);
|
||||
// 设置标记表示已添加临时认证
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("__TEMP_AUTH_ADDED", true);
|
||||
log.debug("已添加临时认证参数: diskType={}, authType={}", type, authType);
|
||||
} else {
|
||||
// 如果没有有效的临时认证参数,回退到使用后台配置
|
||||
if (localMap.containsKey(ConfigConstant.AUTHS)) {
|
||||
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
|
||||
if (auths.containsKey(type)) {
|
||||
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
|
||||
JsonObject jsonObject = auths.getJsonObject(type);
|
||||
if (jsonObject != null) {
|
||||
jsonObject.forEach(entity -> {
|
||||
if (entity == null || entity.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isEmpty(entity.getKey()) || StringUtils.isEmpty(entity.getValue().toString())) {
|
||||
return;
|
||||
}
|
||||
entries.set(StringUtils.trim(entity.getKey()), StringUtils.trim(entity.getValue().toString()));
|
||||
});
|
||||
}
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并设置认证信息(格式: key:value)
|
||||
*/
|
||||
private static void parseAndSetAuthInfo(MultiMap authMap, String authInfo) {
|
||||
if (StringUtils.isBlank(authInfo)) {
|
||||
return;
|
||||
}
|
||||
String[] parts = authInfo.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
String key = parts[0].trim();
|
||||
String value = parts[1].trim();
|
||||
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
|
||||
authMap.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ package cn.qaiu.lz.web.controller;
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.lz.common.cache.CacheManager;
|
||||
import cn.qaiu.lz.common.util.AuthParamCodec;
|
||||
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.ClientLinkResp;
|
||||
import cn.qaiu.lz.web.model.LinkInfoResp;
|
||||
@@ -50,15 +52,18 @@ public class ParserApi {
|
||||
private final ServerApi serverApi = new ServerApi();
|
||||
|
||||
@RouteMapping(value = "/linkInfo", method = RouteMethod.GET)
|
||||
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd) {
|
||||
public Future<LinkInfoResp> parse(HttpServerRequest request, String pwd, String auth) {
|
||||
Promise<LinkInfoResp> promise = Promise.promise();
|
||||
String url = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
|
||||
// 构建链接信息响应,如果有 auth 参数则附加到链接中
|
||||
String authSuffix = (auth != null && !auth.isEmpty()) ? "&auth=" + auth : "";
|
||||
LinkInfoResp build = LinkInfoResp.builder()
|
||||
.downLink(getDownLink(parserCreate, false))
|
||||
.apiLink(getDownLink(parserCreate, true))
|
||||
.viewLink(getViewLink(parserCreate))
|
||||
.downLink(getDownLink(parserCreate, false) + authSuffix)
|
||||
.apiLink(getDownLink(parserCreate, true) + authSuffix)
|
||||
.viewLink(getViewLink(parserCreate) + authSuffix)
|
||||
.shareLinkInfo(shareLinkInfo).build();
|
||||
// 解析次数统计
|
||||
shareLinkInfo.getOtherParam().put("UA",request.headers().get("user-agent"));
|
||||
@@ -214,7 +219,7 @@ public class ParserApi {
|
||||
}
|
||||
|
||||
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
|
||||
new ServerApi().parseJson(request, pwd).onSuccess(res -> {
|
||||
new ServerApi().parseJson(request, pwd, null).onSuccess(res -> {
|
||||
redirect(response, previewURL, res);
|
||||
}).onFailure(e -> {
|
||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
|
||||
@@ -252,10 +257,11 @@ public class ParserApi {
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param pwd 提取码
|
||||
* @param auth 加密的认证参数
|
||||
* @return 客户端下载链接响应
|
||||
*/
|
||||
@RouteMapping(value = "/clientLinks", method = RouteMethod.GET)
|
||||
public Future<ClientLinkResp> getClientLinks(HttpServerRequest request, String pwd) {
|
||||
public Future<ClientLinkResp> getClientLinks(HttpServerRequest request, String pwd, String auth) {
|
||||
Promise<ClientLinkResp> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
@@ -263,6 +269,23 @@ public class ParserApi {
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
|
||||
// 处理认证参数
|
||||
if (auth != null && !auth.isEmpty()) {
|
||||
AuthParam authParam = AuthParamCodec.decode(auth);
|
||||
if (authParam != null && authParam.hasValidAuth()) {
|
||||
URLParamUtil.addTempAuthParam(parserCreate,
|
||||
authParam.getAuthType(),
|
||||
authParam.getPrimaryCredential(),
|
||||
authParam.getPassword(),
|
||||
authParam.getExt1(),
|
||||
authParam.getExt2(),
|
||||
authParam.getExt3(),
|
||||
authParam.getExt4(),
|
||||
authParam.getExt5());
|
||||
log.debug("客户端链接API: 已解码认证参数 authType={}", authParam.getAuthType());
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认方法解析并生成客户端链接
|
||||
parserCreate.createTool().parseWithClientLinks()
|
||||
.onSuccess(clientLinks -> {
|
||||
@@ -345,6 +368,10 @@ public class ParserApi {
|
||||
String directLink = (String) shareLinkInfo.getOtherParam().get("downloadUrl");
|
||||
Map<String, String> supportedClients = buildSupportedClientsMap();
|
||||
FileInfo fileInfo = extractFileInfo(shareLinkInfo);
|
||||
String panType = shareLinkInfo.getType().toUpperCase();
|
||||
|
||||
// 判断是否需要客户端下载和认证需求
|
||||
PanRequirementInfo requirementInfo = getPanRequirementInfo(panType);
|
||||
|
||||
return ClientLinkResp.builder()
|
||||
.success(true)
|
||||
@@ -354,9 +381,66 @@ public class ParserApi {
|
||||
.clientLinks(clientLinks)
|
||||
.supportedClients(supportedClients)
|
||||
.parserInfo(shareLinkInfo.getPanName() + " - " + shareLinkInfo.getType())
|
||||
.panType(panType)
|
||||
.requiresClient(requirementInfo.requiresClient)
|
||||
.authRequirement(requirementInfo.authRequirement)
|
||||
.authHint(requirementInfo.authHint)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 网盘需求信息内部类
|
||||
*/
|
||||
private static class PanRequirementInfo {
|
||||
boolean requiresClient;
|
||||
String authRequirement;
|
||||
String authHint;
|
||||
|
||||
PanRequirementInfo(boolean requiresClient, String authRequirement, String authHint) {
|
||||
this.requiresClient = requiresClient;
|
||||
this.authRequirement = authRequirement;
|
||||
this.authHint = authHint;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网盘需求信息
|
||||
*
|
||||
* @param panType 网盘类型代码(大写)
|
||||
* @return 网盘需求信息
|
||||
*/
|
||||
private PanRequirementInfo getPanRequirementInfo(String panType) {
|
||||
// 需要使用客户端下载的网盘类型(直链需要特殊头部,浏览器无法直接下载)
|
||||
boolean requiresClient = switch (panType) {
|
||||
case "UC", "QK", "PCX", "COW" -> true;
|
||||
default -> false;
|
||||
};
|
||||
|
||||
// 认证需求判断
|
||||
String authRequirement;
|
||||
String authHint;
|
||||
switch (panType) {
|
||||
case "UC", "QK":
|
||||
authRequirement = "required";
|
||||
authHint = "此网盘必须配置认证信息(Cookie/Token)才能正常解析和下载";
|
||||
break;
|
||||
case "FJ":
|
||||
authRequirement = "optional";
|
||||
authHint = "小飞机网盘大文件(>100MB)需要配置认证信息";
|
||||
break;
|
||||
case "IZ":
|
||||
authRequirement = "optional";
|
||||
authHint = "蓝奏优享大文件需要配置认证信息";
|
||||
break;
|
||||
default:
|
||||
authRequirement = "none";
|
||||
authHint = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return new PanRequirementInfo(requiresClient, authRequirement, authHint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建支持的客户端类型映射
|
||||
*
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.qaiu.lz.web.controller;
|
||||
|
||||
import cn.qaiu.lz.common.util.AuthParamCodec;
|
||||
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.service.CacheService;
|
||||
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||
@@ -29,11 +31,14 @@ public class ServerApi {
|
||||
private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class);
|
||||
|
||||
@RouteMapping(value = "/parser", method = RouteMethod.GET, order = 1)
|
||||
public Future<Void> parse(HttpServerResponse response, HttpServerRequest request, RoutingContext rcx, String pwd) {
|
||||
public Future<Void> parse(HttpServerResponse response, HttpServerRequest request, RoutingContext rcx, String pwd, String auth) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
String url = URLParamUtil.parserParams(request);
|
||||
|
||||
cacheService.getCachedByShareUrlAndPwd(url, pwd, JsonObject.of("UA",request.headers().get("user-agent")))
|
||||
// 构建 otherParam,包含 UA 和解码后的认证参数
|
||||
JsonObject otherParam = buildOtherParam(request, auth);
|
||||
|
||||
cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
|
||||
.onSuccess(res -> ResponseUtil.redirect(
|
||||
response.putHeader("nfd-cache-hit", res.getCacheHit().toString())
|
||||
.putHeader("nfd-cache-expires", res.getExpires()),
|
||||
@@ -43,9 +48,10 @@ public class ServerApi {
|
||||
}
|
||||
|
||||
@RouteMapping(value = "/json/parser", method = RouteMethod.GET, order = 1)
|
||||
public Future<CacheLinkInfo> parseJson(HttpServerRequest request, String pwd) {
|
||||
public Future<CacheLinkInfo> parseJson(HttpServerRequest request, String pwd, String auth) {
|
||||
String url = URLParamUtil.parserParams(request);
|
||||
return cacheService.getCachedByShareUrlAndPwd(url, pwd, JsonObject.of("UA",request.headers().get("user-agent")));
|
||||
JsonObject otherParam = buildOtherParam(request, auth);
|
||||
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam);
|
||||
}
|
||||
|
||||
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
|
||||
@@ -77,4 +83,33 @@ public class ServerApi {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 otherParam,包含 UA 和解码后的认证参数
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param auth 加密的认证参数
|
||||
* @return JsonObject
|
||||
*/
|
||||
private JsonObject buildOtherParam(HttpServerRequest request, String auth) {
|
||||
JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent"));
|
||||
|
||||
// 解码认证参数
|
||||
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());
|
||||
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
|
||||
}
|
||||
}
|
||||
|
||||
return otherParam;
|
||||
}
|
||||
}
|
||||
|
||||
166
web-service/src/main/java/cn/qaiu/lz/web/model/AuthParam.java
Normal file
166
web-service/src/main/java/cn/qaiu/lz/web/model/AuthParam.java
Normal file
@@ -0,0 +1,166 @@
|
||||
package cn.qaiu.lz.web.model;
|
||||
|
||||
import cn.qaiu.lz.common.ToJson;
|
||||
import io.vertx.codegen.annotations.DataObject;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 认证参数模型
|
||||
* 用于接口传参时携带临时认证信息
|
||||
* <p>
|
||||
* 传参格式: auth=URL_ENCODE(BASE64(AES_ENCRYPT(JSON)))
|
||||
* JSON格式: {"username":"xxx","password":"xxx","token":"xxx","cookie":"xxx",...}
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2026/2/5
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DataObject
|
||||
public class AuthParam implements ToJson {
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
* <ul>
|
||||
* <li>accesstoken - 使用 accessToken 认证</li>
|
||||
* <li>cookie - 使用 Cookie 认证</li>
|
||||
* <li>authorization - 使用 Authorization 头认证</li>
|
||||
* <li>password/username_password - 使用用户名密码认证</li>
|
||||
* <li>custom - 自定义认证(使用 ext1-ext5 扩展字段)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String authType;
|
||||
|
||||
/**
|
||||
* 用户名(用于 password/username_password 认证类型)
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码(用于 password/username_password 认证类型)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Token(accessToken/authorization/通用token)
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* Cookie 字符串
|
||||
*/
|
||||
private String cookie;
|
||||
|
||||
/**
|
||||
* 授权信息(Authorization 头内容)
|
||||
*/
|
||||
private String auth;
|
||||
|
||||
/**
|
||||
* 扩展字段1(用于 custom 认证类型)
|
||||
* 格式: key:value
|
||||
*/
|
||||
private String ext1;
|
||||
|
||||
/**
|
||||
* 扩展字段2(用于 custom 认证类型)
|
||||
* 格式: key:value
|
||||
*/
|
||||
private String ext2;
|
||||
|
||||
/**
|
||||
* 扩展字段3(用于 custom 认证类型)
|
||||
* 格式: key:value
|
||||
*/
|
||||
private String ext3;
|
||||
|
||||
/**
|
||||
* 扩展字段4(用于 custom 认证类型)
|
||||
* 格式: key:value
|
||||
*/
|
||||
private String ext4;
|
||||
|
||||
/**
|
||||
* 扩展字段5(用于 custom 认证类型)
|
||||
* 格式: key:value
|
||||
*/
|
||||
private String ext5;
|
||||
|
||||
/**
|
||||
* 从 JsonObject 构造
|
||||
*/
|
||||
public AuthParam(JsonObject json) {
|
||||
if (json == null) {
|
||||
return;
|
||||
}
|
||||
this.authType = json.getString("authType");
|
||||
this.username = json.getString("username");
|
||||
this.password = json.getString("password");
|
||||
this.token = json.getString("token");
|
||||
this.cookie = json.getString("cookie");
|
||||
this.auth = json.getString("auth");
|
||||
this.ext1 = json.getString("ext1");
|
||||
this.ext2 = json.getString("ext2");
|
||||
this.ext3 = json.getString("ext3");
|
||||
this.ext4 = json.getString("ext4");
|
||||
this.ext5 = json.getString("ext5");
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 JsonObject
|
||||
*/
|
||||
public JsonObject toJsonObject() {
|
||||
JsonObject json = new JsonObject();
|
||||
if (authType != null) json.put("authType", authType);
|
||||
if (username != null) json.put("username", username);
|
||||
if (password != null) json.put("password", password);
|
||||
if (token != null) json.put("token", token);
|
||||
if (cookie != null) json.put("cookie", cookie);
|
||||
if (auth != null) json.put("auth", auth);
|
||||
if (ext1 != null) json.put("ext1", ext1);
|
||||
if (ext2 != null) json.put("ext2", ext2);
|
||||
if (ext3 != null) json.put("ext3", ext3);
|
||||
if (ext4 != null) json.put("ext4", ext4);
|
||||
if (ext5 != null) json.put("ext5", ext5);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有有效的认证信息
|
||||
*/
|
||||
public boolean hasValidAuth() {
|
||||
return authType != null ||
|
||||
username != null ||
|
||||
password != null ||
|
||||
token != null ||
|
||||
cookie != null ||
|
||||
auth != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主要的认证凭证(token/cookie/auth)
|
||||
* 优先级: token > cookie > auth > username
|
||||
*/
|
||||
public String getPrimaryCredential() {
|
||||
if (token != null && !token.isEmpty()) {
|
||||
return token;
|
||||
}
|
||||
if (cookie != null && !cookie.isEmpty()) {
|
||||
return cookie;
|
||||
}
|
||||
if (auth != null && !auth.isEmpty()) {
|
||||
return auth;
|
||||
}
|
||||
if (username != null && !username.isEmpty()) {
|
||||
return username;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import io.vertx.core.json.jackson.DatabindCodec;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2024/9/11 16:06
|
||||
@@ -52,6 +55,16 @@ public class CacheLinkInfo implements ToJson {
|
||||
private Long expiration;
|
||||
|
||||
private FileInfo fileInfo;
|
||||
|
||||
/**
|
||||
* 其他参数,包含:
|
||||
* - downloadHeaders: 下载请求头 Map<String, String>
|
||||
* - aria2Command: aria2 命令行命令
|
||||
* - aria2JsonRpc: aria2 JSON-RPC 请求体
|
||||
* - curlCommand: curl 命令
|
||||
*/
|
||||
@TableGenIgnore
|
||||
private Map<String, Object> otherParam;
|
||||
|
||||
|
||||
// 使用 JsonObject 构造
|
||||
@@ -74,6 +87,15 @@ public class CacheLinkInfo implements ToJson {
|
||||
this.setFileInfo(mapper.convertValue(json.getJsonObject("fileInfo"), FileInfo.class));
|
||||
}
|
||||
this.setCacheHit(json.getBoolean("cacheHit", false));
|
||||
|
||||
// 初始化 otherParam
|
||||
this.otherParam = new HashMap<>();
|
||||
if (json.containsKey("otherParam")) {
|
||||
JsonObject otherJson = json.getJsonObject("otherParam");
|
||||
if (otherJson != null) {
|
||||
this.otherParam.putAll(otherJson.getMap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,4 +59,28 @@ public class ClientLinkResp {
|
||||
* 解析信息
|
||||
*/
|
||||
private String parserInfo;
|
||||
|
||||
/**
|
||||
* 网盘类型代码
|
||||
*/
|
||||
private String panType;
|
||||
|
||||
/**
|
||||
* 是否必须使用客户端下载(直链需要特殊头部,浏览器无法直接下载)
|
||||
* 适用于:UC、QK、PCX、COW等
|
||||
*/
|
||||
private boolean requiresClient;
|
||||
|
||||
/**
|
||||
* 认证需求级别:
|
||||
* - "none": 不需要认证
|
||||
* - "required": 必须认证(UC、QK)
|
||||
* - "optional": 可选认证,大文件需要(FJ、IZ)
|
||||
*/
|
||||
private String authRequirement;
|
||||
|
||||
/**
|
||||
* 认证提示信息
|
||||
*/
|
||||
private String authHint;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||
import cn.qaiu.lz.web.service.CacheService;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.vx.core.annotaions.Service;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
@@ -18,6 +20,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -27,13 +31,21 @@ public class CacheServiceImpl implements CacheService {
|
||||
|
||||
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
|
||||
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
// 认证、域名相关(检查是否已经添加过参数,避免重复调用)
|
||||
if (!parserCreate.getShareLinkInfo().getOtherParam().containsKey("__PARAMS_ADDED")) {
|
||||
URLParamUtil.addParam(parserCreate);
|
||||
parserCreate.getShareLinkInfo().getOtherParam().put("__PARAMS_ADDED", true);
|
||||
}
|
||||
|
||||
Promise<CacheLinkInfo> promise = Promise.promise();
|
||||
// 构建组合的缓存key
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
// 尝试从缓存中获取
|
||||
String cacheKey = shareLinkInfo.getCacheKey();
|
||||
|
||||
// 使用配置文件中的默认缓存时长
|
||||
final int effectiveCacheDuration = CacheConfigLoader.getDuration(shareLinkInfo.getType());
|
||||
|
||||
// 尝试从缓存中获取
|
||||
cacheManager.get(cacheKey).onSuccess(result -> {
|
||||
// 判断是否已过期
|
||||
// 未命中或者过期
|
||||
@@ -49,36 +61,64 @@ public class CacheServiceImpl implements CacheService {
|
||||
return;
|
||||
}
|
||||
tool.parse().onSuccess(redirectUrl -> {
|
||||
long expires = System.currentTimeMillis() +
|
||||
CacheConfigLoader.getDuration(shareLinkInfo.getType()) * 60 * 1000L;
|
||||
// 使用 effectiveCacheDuration
|
||||
long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L;
|
||||
result.setDirectLink(redirectUrl);
|
||||
// 设置返回结果的过期时间
|
||||
result.setExpiration(expires);
|
||||
result.setExpires(generateDate(expires));
|
||||
|
||||
// 调试日志:检查解析器返回的otherParam
|
||||
log.info("[解析完成] shareKey={}, otherParam.keys={}, hasFileInfo={}",
|
||||
cacheKey,
|
||||
shareLinkInfo.getOtherParam().keySet(),
|
||||
shareLinkInfo.getOtherParam().containsKey("fileInfo"));
|
||||
|
||||
CacheLinkInfo cacheLinkInfo = new CacheLinkInfo(JsonObject.of(
|
||||
"directLink", redirectUrl,
|
||||
"expiration", expires,
|
||||
"shareKey", cacheKey
|
||||
));
|
||||
// 提取并设置文件信息
|
||||
if (shareLinkInfo.getOtherParam().containsKey("fileInfo")) {
|
||||
try {
|
||||
FileInfo fileInfo = (FileInfo) shareLinkInfo.getOtherParam().get("fileInfo");
|
||||
result.setFileInfo(fileInfo);
|
||||
cacheLinkInfo.setFileInfo(fileInfo);
|
||||
} catch (Exception ignored) {
|
||||
log.error("文件对象转换异常");
|
||||
log.info("[设置文件信息] shareKey={}, fileName={}, size={}",
|
||||
cacheKey, fileInfo.getFileName(), fileInfo.getSize());
|
||||
} catch (Exception e) {
|
||||
log.error("文件对象转换异常: shareKey={}", cacheKey, e);
|
||||
}
|
||||
} else {
|
||||
log.warn("[文件信息缺失] 解析器未返回fileInfo: shareKey={}, otherParam.keys={}",
|
||||
cacheKey, shareLinkInfo.getOtherParam().keySet());
|
||||
}
|
||||
// 传递 downloadHeaders 并生成下载命令
|
||||
processDownloadHeaders(shareLinkInfo, cacheLinkInfo, result);
|
||||
promise.complete(result);
|
||||
// 更新缓存
|
||||
// 将直链存储到缓存
|
||||
cacheManager.cacheShareLink(cacheLinkInfo);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.API_PARSER_TOTAL).onFailure(Throwable::printStackTrace);
|
||||
}).onFailure(promise::fail);
|
||||
} else {
|
||||
// 缓存命中,生成过期时间并生成下载命令
|
||||
result.setExpires(generateDate(result.getExpiration()));
|
||||
|
||||
// 初始化 otherParam(如果为空)
|
||||
if (result.getOtherParam() == null) {
|
||||
result.setOtherParam(new HashMap<>());
|
||||
}
|
||||
|
||||
// 生成下载命令(aria2、curl)
|
||||
generateDownloadCommands(result);
|
||||
|
||||
promise.complete(result);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL).onFailure(Throwable::printStackTrace);
|
||||
cacheManager.updateTotalByField(cacheKey, CacheTotalField.CACHE_HIT_TOTAL)
|
||||
.onFailure(Throwable::printStackTrace);
|
||||
}
|
||||
}).onFailure(t -> promise.fail(t.fillInStackTrace()));
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@@ -86,6 +126,128 @@ public class CacheServiceImpl implements CacheService {
|
||||
return DateFormatUtils.format(new Date(ts), "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下载请求头并生成下载命令
|
||||
* 从 shareLinkInfo 中提取 downloadHeaders,传递到 cacheLinkInfo 和 result
|
||||
*/
|
||||
private void processDownloadHeaders(ShareLinkInfo shareLinkInfo, CacheLinkInfo cacheLinkInfo,
|
||||
CacheLinkInfo result) {
|
||||
try {
|
||||
// 提取 downloadHeaders(如果不存在,使用空Map)
|
||||
Map<String, String> downloadHeaders = new HashMap<>();
|
||||
|
||||
if (shareLinkInfo.getOtherParam() != null
|
||||
&& shareLinkInfo.getOtherParam().containsKey("downloadHeaders")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> headers = (Map<String, String>) shareLinkInfo.getOtherParam().get("downloadHeaders");
|
||||
if (headers != null) {
|
||||
downloadHeaders = headers;
|
||||
log.info("从shareLinkInfo提取downloadHeaders: shareKey={}, 请求头数量={}",
|
||||
cacheLinkInfo.getShareKey(), downloadHeaders.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 otherParam
|
||||
if (cacheLinkInfo.getOtherParam() == null) {
|
||||
cacheLinkInfo.setOtherParam(new HashMap<>());
|
||||
}
|
||||
if (result.getOtherParam() == null) {
|
||||
result.setOtherParam(new HashMap<>());
|
||||
}
|
||||
|
||||
// 传递 downloadHeaders 到两个对象
|
||||
cacheLinkInfo.getOtherParam().put("downloadHeaders", downloadHeaders);
|
||||
result.getOtherParam().put("downloadHeaders", downloadHeaders);
|
||||
|
||||
// 使用已有的工具类生成下载命令
|
||||
generateCommandsFromShareLinkInfo(shareLinkInfo, cacheLinkInfo, result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理下载请求头异常: shareKey={}", cacheLinkInfo.getShareKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 ClientLinkGeneratorFactory 生成下载命令
|
||||
*/
|
||||
private void generateCommandsFromShareLinkInfo(ShareLinkInfo shareLinkInfo,
|
||||
CacheLinkInfo cacheLinkInfo,
|
||||
CacheLinkInfo result) {
|
||||
try {
|
||||
// 使用已有的 ClientLinkGeneratorFactory 生成命令
|
||||
Map<ClientLinkType, String> clientLinks = ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
|
||||
// 提取各命令并存储
|
||||
String curlCommand = clientLinks.get(ClientLinkType.CURL);
|
||||
String aria2Command = clientLinks.get(ClientLinkType.ARIA2);
|
||||
String thunderLink = clientLinks.get(ClientLinkType.THUNDER);
|
||||
|
||||
// 设置命令到 cacheLinkInfo 和 result
|
||||
if (curlCommand != null) {
|
||||
cacheLinkInfo.getOtherParam().put("curlCommand", curlCommand);
|
||||
result.getOtherParam().put("curlCommand", curlCommand);
|
||||
}
|
||||
if (aria2Command != null) {
|
||||
cacheLinkInfo.getOtherParam().put("aria2Command", aria2Command);
|
||||
result.getOtherParam().put("aria2Command", aria2Command);
|
||||
}
|
||||
if (thunderLink != null) {
|
||||
cacheLinkInfo.getOtherParam().put("thunderLink", thunderLink);
|
||||
result.getOtherParam().put("thunderLink", thunderLink);
|
||||
}
|
||||
|
||||
log.debug("已生成下载命令: shareKey={}, commands={}",
|
||||
cacheLinkInfo.getShareKey(), clientLinks.keySet());
|
||||
} catch (Exception e) {
|
||||
log.error("生成下载命令异常: shareKey={}", cacheLinkInfo.getShareKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成下载命令(缓存命中时)
|
||||
*/
|
||||
private void generateDownloadCommands(CacheLinkInfo cacheLinkInfo) {
|
||||
if (cacheLinkInfo.getDirectLink() == null || cacheLinkInfo.getDirectLink().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建临时 ShareLinkInfo 用于生成命令
|
||||
ShareLinkInfo tempInfo = ShareLinkInfo.newBuilder()
|
||||
.shareUrl(cacheLinkInfo.getDirectLink())
|
||||
.build();
|
||||
tempInfo.getOtherParam().put("downloadUrl", cacheLinkInfo.getDirectLink());
|
||||
|
||||
// 复制 downloadHeaders
|
||||
if (cacheLinkInfo.getOtherParam() != null
|
||||
&& cacheLinkInfo.getOtherParam().containsKey("downloadHeaders")) {
|
||||
tempInfo.getOtherParam().put("downloadHeaders", cacheLinkInfo.getOtherParam().get("downloadHeaders"));
|
||||
}
|
||||
|
||||
// 复制文件信息
|
||||
if (cacheLinkInfo.getFileInfo() != null) {
|
||||
tempInfo.getOtherParam().put("fileInfo", cacheLinkInfo.getFileInfo());
|
||||
}
|
||||
|
||||
// 使用 ClientLinkGeneratorFactory 生成命令
|
||||
Map<ClientLinkType, String> clientLinks = ClientLinkGeneratorFactory.generateAll(tempInfo);
|
||||
|
||||
// 存储命令
|
||||
if (clientLinks.containsKey(ClientLinkType.CURL)) {
|
||||
cacheLinkInfo.getOtherParam().put("curlCommand", clientLinks.get(ClientLinkType.CURL));
|
||||
}
|
||||
if (clientLinks.containsKey(ClientLinkType.ARIA2)) {
|
||||
cacheLinkInfo.getOtherParam().put("aria2Command", clientLinks.get(ClientLinkType.ARIA2));
|
||||
}
|
||||
if (clientLinks.containsKey(ClientLinkType.THUNDER)) {
|
||||
cacheLinkInfo.getOtherParam().put("thunderLink", clientLinks.get(ClientLinkType.THUNDER));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成下载命令异常: shareKey={}", cacheLinkInfo.getShareKey(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<CacheLinkInfo> getCachedByShareKeyAndPwd(String type, String shareKey, String pwd, JsonObject otherParam) {
|
||||
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey(shareKey).setShareLinkInfoPwd(pwd);
|
||||
@@ -97,6 +259,21 @@ public class CacheServiceImpl implements CacheService {
|
||||
public Future<CacheLinkInfo> getCachedByShareUrlAndPwd(String shareUrl, String pwd, JsonObject otherParam) {
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
|
||||
|
||||
// 检查是否有临时认证参数
|
||||
if (otherParam.containsKey("authType") || otherParam.containsKey("authToken")) {
|
||||
log.debug("从otherParam中检测到临时认证参数");
|
||||
URLParamUtil.addTempAuthParam(parserCreate,
|
||||
otherParam.getString("authType"),
|
||||
otherParam.getString("authToken"),
|
||||
otherParam.getString("authPassword"),
|
||||
otherParam.getString("authInfo1"),
|
||||
otherParam.getString("authInfo2"),
|
||||
otherParam.getString("authInfo3"),
|
||||
otherParam.getString("authInfo4"),
|
||||
otherParam.getString("authInfo5"));
|
||||
}
|
||||
|
||||
return getAndSaveCachedShareLink(parserCreate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ server:
|
||||
domainName: http://127.0.0.1:6401
|
||||
# 预览服务URL
|
||||
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
|
||||
# auth参数加密密钥(16位AES密钥)
|
||||
authEncryptKey: 'nfd_auth_key2026'
|
||||
# domainName: https://lz.qaiu.top
|
||||
|
||||
# 反向代理服务器配置路径(不用加后缀)
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
package cn.qaiu.lz.common.util;
|
||||
|
||||
import cn.qaiu.lz.web.model.AuthParam;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* 认证参数编解码测试
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2026/2/5
|
||||
*/
|
||||
public class AuthParamCodecTest {
|
||||
|
||||
// 测试用的固定密钥
|
||||
private static final String TEST_KEY = "nfd_auth_key2026";
|
||||
|
||||
@Test
|
||||
public void testAuthParamModel() {
|
||||
// 测试构建器
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType("accesstoken")
|
||||
.token("test_token_123")
|
||||
.build();
|
||||
|
||||
assertEquals("accesstoken", authParam.getAuthType());
|
||||
assertEquals("test_token_123", authParam.getToken());
|
||||
assertTrue(authParam.hasValidAuth());
|
||||
assertEquals("test_token_123", authParam.getPrimaryCredential());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthParamFromJson() {
|
||||
JsonObject json = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpass");
|
||||
|
||||
AuthParam authParam = new AuthParam(json);
|
||||
|
||||
assertEquals("password", authParam.getAuthType());
|
||||
assertEquals("testuser", authParam.getUsername());
|
||||
assertEquals("testpass", authParam.getPassword());
|
||||
assertTrue(authParam.hasValidAuth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthParamToJson() {
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType("cookie")
|
||||
.token("session=abc123")
|
||||
.ext1("key1:value1")
|
||||
.build();
|
||||
|
||||
JsonObject json = authParam.toJsonObject();
|
||||
|
||||
assertEquals("cookie", json.getString("authType"));
|
||||
assertEquals("session=abc123", json.getString("token"));
|
||||
assertEquals("key1:value1", json.getString("ext1"));
|
||||
assertNull(json.getString("username")); // 未设置的字段应为 null
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManualEncodeDecodeToken() throws Exception {
|
||||
// 构建原始 JSON
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "my_access_token_12345");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
System.out.println("原始 JSON: " + jsonStr);
|
||||
|
||||
// Step 1: AES 加密 + Base64 编码
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
System.out.println("AES+Base64 编码: " + base64Encoded);
|
||||
|
||||
// Step 2: URL 编码
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
System.out.println("URL 编码: " + urlEncoded);
|
||||
|
||||
// ===== 解码流程 =====
|
||||
|
||||
// Step 1: URL 解码
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
assertEquals(base64Encoded, urlDecoded);
|
||||
|
||||
// Step 2: Base64 解码
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
|
||||
// Step 3: AES 解密
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
System.out.println("解密后 JSON: " + decryptedJson);
|
||||
|
||||
// Step 4: JSON 解析
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("accesstoken", decoded.getString("authType"));
|
||||
assertEquals("my_access_token_12345", decoded.getString("token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManualEncodeDecodePassword() throws Exception {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpassword123");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
System.out.println("用户名密码认证 - 加密结果: " + urlEncoded);
|
||||
|
||||
// 解码验证
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("password", decoded.getString("authType"));
|
||||
assertEquals("testuser", decoded.getString("username"));
|
||||
assertEquals("testpassword123", decoded.getString("password"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManualEncodeDecodeCookie() throws Exception {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "session_id=abc123xyz; user_token=def456");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
System.out.println("Cookie 认证 - 加密结果: " + urlEncoded);
|
||||
|
||||
// 解码验证
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("cookie", decoded.getString("authType"));
|
||||
assertEquals("session_id=abc123xyz; user_token=def456", decoded.getString("token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManualEncodeDecodeCustom() throws Exception {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "custom")
|
||||
.put("token", "main_token")
|
||||
.put("ext1", "refresh_token:rt_12345")
|
||||
.put("ext2", "device_id:device_abc")
|
||||
.put("ext3", "app_version:1.0.0");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
System.out.println("自定义认证 - 加密结果: " + urlEncoded);
|
||||
|
||||
// 解码验证
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("custom", decoded.getString("authType"));
|
||||
assertEquals("main_token", decoded.getString("token"));
|
||||
assertEquals("refresh_token:rt_12345", decoded.getString("ext1"));
|
||||
assertEquals("device_id:device_abc", decoded.getString("ext2"));
|
||||
assertEquals("app_version:1.0.0", decoded.getString("ext3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrimaryCredentialPriority() {
|
||||
// token 优先级最高
|
||||
AuthParam authParam1 = AuthParam.builder()
|
||||
.token("token_value")
|
||||
.cookie("cookie_value")
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("token_value", authParam1.getPrimaryCredential());
|
||||
|
||||
// 没有 token 时,cookie 优先
|
||||
AuthParam authParam2 = AuthParam.builder()
|
||||
.cookie("cookie_value")
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("cookie_value", authParam2.getPrimaryCredential());
|
||||
|
||||
// 没有 token 和 cookie 时,auth 优先
|
||||
AuthParam authParam3 = AuthParam.builder()
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("auth_value", authParam3.getPrimaryCredential());
|
||||
|
||||
// 只有 username 时
|
||||
AuthParam authParam4 = AuthParam.builder()
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("user_value", authParam4.getPrimaryCredential());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyAuthParam() {
|
||||
AuthParam authParam = new AuthParam();
|
||||
assertFalse(authParam.hasValidAuth());
|
||||
assertNull(authParam.getPrimaryCredential());
|
||||
|
||||
AuthParam authParam2 = new AuthParam(null);
|
||||
assertFalse(authParam2.hasValidAuth());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpecialCharacters() throws Exception {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "name=中文测试; value=!@#$%^&*()_+-={}|[]\\:\";'<>?,./");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
System.out.println("特殊字符测试 - 加密结果: " + urlEncoded);
|
||||
|
||||
// 解码验证
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("cookie", decoded.getString("authType"));
|
||||
assertEquals("name=中文测试; value=!@#$%^&*()_+-={}|[]\\:\";'<>?,./", decoded.getString("token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateAuthForApiCall() throws Exception {
|
||||
// 模拟实际使用场景
|
||||
JsonObject authJson = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "real_token_for_api_test");
|
||||
|
||||
String jsonStr = authJson.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String auth = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
// 构建完整的 API URL
|
||||
String baseUrl = "http://127.0.0.1:6400/parser";
|
||||
String shareUrl = "https://www.lanzoux.com/test123";
|
||||
String pwd = "abcd";
|
||||
|
||||
String fullUrl = String.format("%s?url=%s&pwd=%s&auth=%s",
|
||||
baseUrl,
|
||||
URLEncoder.encode(shareUrl, StandardCharsets.UTF_8),
|
||||
pwd,
|
||||
auth);
|
||||
|
||||
System.out.println("=== 生成的完整 API 调用 URL ===");
|
||||
System.out.println(fullUrl);
|
||||
System.out.println("=== auth 参数值 ===");
|
||||
System.out.println(auth);
|
||||
|
||||
// 验证 URL 格式正确
|
||||
assertTrue(fullUrl.contains("url="));
|
||||
assertTrue(fullUrl.contains("pwd="));
|
||||
assertTrue(fullUrl.contains("auth="));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void printEncryptionExamples() throws Exception {
|
||||
System.out.println("\n========== 认证参数加密示例 ==========\n");
|
||||
|
||||
// 1. AccessToken
|
||||
JsonObject tokenAuth = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "example_access_token");
|
||||
String tokenEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(tokenAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("1. AccessToken 认证:");
|
||||
System.out.println(" 原始: " + tokenAuth.encode());
|
||||
System.out.println(" 加密: " + tokenEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 2. Cookie
|
||||
JsonObject cookieAuth = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "session=abc123");
|
||||
String cookieEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(cookieAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("2. Cookie 认证:");
|
||||
System.out.println(" 原始: " + cookieAuth.encode());
|
||||
System.out.println(" 加密: " + cookieEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 3. 用户名密码
|
||||
JsonObject passwordAuth = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpass");
|
||||
String passwordEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(passwordAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("3. 用户名密码认证:");
|
||||
System.out.println(" 原始: " + passwordAuth.encode());
|
||||
System.out.println(" 加密: " + passwordEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 4. 自定义
|
||||
JsonObject customAuth = new JsonObject()
|
||||
.put("authType", "custom")
|
||||
.put("token", "main_token")
|
||||
.put("ext1", "key1:value1");
|
||||
String customEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(customAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("4. 自定义认证:");
|
||||
System.out.println(" 原始: " + customAuth.encode());
|
||||
System.out.println(" 加密: " + customEncrypted);
|
||||
System.out.println();
|
||||
|
||||
System.out.println("========== 示例结束 ==========\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package cn.qaiu.lz.common.util;
|
||||
|
||||
import cn.qaiu.lz.web.model.AuthParam;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 认证参数编解码测试 - 独立运行版本
|
||||
* 运行方法:在 IDE 中直接运行此类的 main 方法
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2026/2/5
|
||||
*/
|
||||
public class AuthParamCodecTestMain {
|
||||
|
||||
private static final String TEST_KEY = "nfd_auth_key2026";
|
||||
private static int passCount = 0;
|
||||
private static int failCount = 0;
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("========== 认证参数编解码测试 ==========\n");
|
||||
|
||||
testAuthParamModel();
|
||||
testAuthParamFromJson();
|
||||
testAuthParamToJson();
|
||||
testManualEncodeDecodeToken();
|
||||
testManualEncodeDecodePassword();
|
||||
testManualEncodeDecodeCookie();
|
||||
testManualEncodeDecodeCustom();
|
||||
testPrimaryCredentialPriority();
|
||||
testEmptyAuthParam();
|
||||
testSpecialCharacters();
|
||||
testGenerateAuthForApiCall();
|
||||
printEncryptionExamples();
|
||||
|
||||
System.out.println("\n========== 测试结果汇总 ==========");
|
||||
System.out.println("通过: " + passCount + ", 失败: " + failCount);
|
||||
System.out.println("========== 测试结束 ==========\n");
|
||||
}
|
||||
|
||||
private static void testAuthParamModel() {
|
||||
System.out.println("测试: AuthParam 模型基本功能");
|
||||
try {
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType("accesstoken")
|
||||
.token("test_token_123")
|
||||
.build();
|
||||
|
||||
assertEquals("accesstoken", authParam.getAuthType());
|
||||
assertEquals("test_token_123", authParam.getToken());
|
||||
assertTrue(authParam.hasValidAuth());
|
||||
assertEquals("test_token_123", authParam.getPrimaryCredential());
|
||||
pass();
|
||||
} catch (AssertionError e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testAuthParamFromJson() {
|
||||
System.out.println("测试: AuthParam 从 JsonObject 构造");
|
||||
try {
|
||||
JsonObject json = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpass");
|
||||
|
||||
AuthParam authParam = new AuthParam(json);
|
||||
|
||||
assertEquals("password", authParam.getAuthType());
|
||||
assertEquals("testuser", authParam.getUsername());
|
||||
assertEquals("testpass", authParam.getPassword());
|
||||
assertTrue(authParam.hasValidAuth());
|
||||
pass();
|
||||
} catch (AssertionError e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testAuthParamToJson() {
|
||||
System.out.println("测试: AuthParam 转换为 JsonObject");
|
||||
try {
|
||||
AuthParam authParam = AuthParam.builder()
|
||||
.authType("cookie")
|
||||
.token("session=abc123")
|
||||
.ext1("key1:value1")
|
||||
.build();
|
||||
|
||||
JsonObject json = authParam.toJsonObject();
|
||||
|
||||
assertEquals("cookie", json.getString("authType"));
|
||||
assertEquals("session=abc123", json.getString("token"));
|
||||
assertEquals("key1:value1", json.getString("ext1"));
|
||||
assertNull(json.getString("username"));
|
||||
pass();
|
||||
} catch (AssertionError e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testManualEncodeDecodeToken() {
|
||||
System.out.println("测试: 手动编解码流程 - Token 认证");
|
||||
try {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "my_access_token_12345");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
// 解码验证
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
assertEquals(base64Encoded, urlDecoded);
|
||||
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("accesstoken", decoded.getString("authType"));
|
||||
assertEquals("my_access_token_12345", decoded.getString("token"));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testManualEncodeDecodePassword() {
|
||||
System.out.println("测试: 手动编解码流程 - 用户名密码认证");
|
||||
try {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpassword123");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("password", decoded.getString("authType"));
|
||||
assertEquals("testuser", decoded.getString("username"));
|
||||
assertEquals("testpassword123", decoded.getString("password"));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testManualEncodeDecodeCookie() {
|
||||
System.out.println("测试: 手动编解码流程 - Cookie 认证");
|
||||
try {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "session_id=abc123xyz; user_token=def456");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("cookie", decoded.getString("authType"));
|
||||
assertEquals("session_id=abc123xyz; user_token=def456", decoded.getString("token"));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testManualEncodeDecodeCustom() {
|
||||
System.out.println("测试: 手动编解码流程 - 自定义认证");
|
||||
try {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "custom")
|
||||
.put("token", "main_token")
|
||||
.put("ext1", "refresh_token:rt_12345")
|
||||
.put("ext2", "device_id:device_abc")
|
||||
.put("ext3", "app_version:1.0.0");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("custom", decoded.getString("authType"));
|
||||
assertEquals("main_token", decoded.getString("token"));
|
||||
assertEquals("refresh_token:rt_12345", decoded.getString("ext1"));
|
||||
assertEquals("device_id:device_abc", decoded.getString("ext2"));
|
||||
assertEquals("app_version:1.0.0", decoded.getString("ext3"));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testPrimaryCredentialPriority() {
|
||||
System.out.println("测试: 主要凭证优先级");
|
||||
try {
|
||||
// token 优先级最高
|
||||
AuthParam authParam1 = AuthParam.builder()
|
||||
.token("token_value")
|
||||
.cookie("cookie_value")
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("token_value", authParam1.getPrimaryCredential());
|
||||
|
||||
// 没有 token 时,cookie 优先
|
||||
AuthParam authParam2 = AuthParam.builder()
|
||||
.cookie("cookie_value")
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("cookie_value", authParam2.getPrimaryCredential());
|
||||
|
||||
// 没有 token 和 cookie 时,auth 优先
|
||||
AuthParam authParam3 = AuthParam.builder()
|
||||
.auth("auth_value")
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("auth_value", authParam3.getPrimaryCredential());
|
||||
|
||||
// 只有 username 时
|
||||
AuthParam authParam4 = AuthParam.builder()
|
||||
.username("user_value")
|
||||
.build();
|
||||
assertEquals("user_value", authParam4.getPrimaryCredential());
|
||||
pass();
|
||||
} catch (AssertionError e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testEmptyAuthParam() {
|
||||
System.out.println("测试: 空认证参数");
|
||||
try {
|
||||
AuthParam authParam = new AuthParam();
|
||||
assertFalse(authParam.hasValidAuth());
|
||||
assertNull(authParam.getPrimaryCredential());
|
||||
|
||||
AuthParam authParam2 = new AuthParam(null);
|
||||
assertFalse(authParam2.hasValidAuth());
|
||||
pass();
|
||||
} catch (AssertionError e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testSpecialCharacters() {
|
||||
System.out.println("测试: 特殊字符处理");
|
||||
try {
|
||||
JsonObject original = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "name=中文测试; value=!@#$%^&*()_+-={}|[]\\:\";'<>?,./");
|
||||
|
||||
String jsonStr = original.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String urlEncoded = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
String urlDecoded = URLDecoder.decode(urlEncoded, StandardCharsets.UTF_8);
|
||||
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
|
||||
String decryptedJson = AESUtils.decryptByAES(base64Decoded, TEST_KEY);
|
||||
|
||||
JsonObject decoded = new JsonObject(decryptedJson);
|
||||
assertEquals("cookie", decoded.getString("authType"));
|
||||
assertEquals("name=中文测试; value=!@#$%^&*()_+-={}|[]\\:\";'<>?,./", decoded.getString("token"));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testGenerateAuthForApiCall() {
|
||||
System.out.println("测试: 生成可用于接口调用的 auth 参数");
|
||||
try {
|
||||
JsonObject authJson = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "real_token_for_api_test");
|
||||
|
||||
String jsonStr = authJson.encode();
|
||||
String base64Encoded = AESUtils.encryptBase64ByAES(jsonStr, TEST_KEY);
|
||||
String auth = URLEncoder.encode(base64Encoded, StandardCharsets.UTF_8);
|
||||
|
||||
String baseUrl = "http://127.0.0.1:6400/parser";
|
||||
String shareUrl = "https://www.lanzoux.com/test123";
|
||||
String pwd = "abcd";
|
||||
|
||||
String fullUrl = String.format("%s?url=%s&pwd=%s&auth=%s",
|
||||
baseUrl,
|
||||
URLEncoder.encode(shareUrl, StandardCharsets.UTF_8),
|
||||
pwd,
|
||||
auth);
|
||||
|
||||
System.out.println(" 生成的完整 API 调用 URL:");
|
||||
System.out.println(" " + fullUrl);
|
||||
|
||||
assertTrue(fullUrl.contains("url="));
|
||||
assertTrue(fullUrl.contains("pwd="));
|
||||
assertTrue(fullUrl.contains("auth="));
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void printEncryptionExamples() {
|
||||
System.out.println("\n========== 认证参数加密示例(供接口调用参考)==========\n");
|
||||
try {
|
||||
// 1. AccessToken
|
||||
JsonObject tokenAuth = new JsonObject()
|
||||
.put("authType", "accesstoken")
|
||||
.put("token", "example_access_token");
|
||||
String tokenEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(tokenAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("1. AccessToken 认证:");
|
||||
System.out.println(" 原始: " + tokenAuth.encode());
|
||||
System.out.println(" 加密: " + tokenEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 2. Cookie
|
||||
JsonObject cookieAuth = new JsonObject()
|
||||
.put("authType", "cookie")
|
||||
.put("token", "session=abc123");
|
||||
String cookieEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(cookieAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("2. Cookie 认证:");
|
||||
System.out.println(" 原始: " + cookieAuth.encode());
|
||||
System.out.println(" 加密: " + cookieEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 3. 用户名密码
|
||||
JsonObject passwordAuth = new JsonObject()
|
||||
.put("authType", "password")
|
||||
.put("username", "testuser")
|
||||
.put("password", "testpass");
|
||||
String passwordEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(passwordAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("3. 用户名密码认证:");
|
||||
System.out.println(" 原始: " + passwordAuth.encode());
|
||||
System.out.println(" 加密: " + passwordEncrypted);
|
||||
System.out.println();
|
||||
|
||||
// 4. 自定义
|
||||
JsonObject customAuth = new JsonObject()
|
||||
.put("authType", "custom")
|
||||
.put("token", "main_token")
|
||||
.put("ext1", "key1:value1");
|
||||
String customEncrypted = URLEncoder.encode(
|
||||
AESUtils.encryptBase64ByAES(customAuth.encode(), TEST_KEY),
|
||||
StandardCharsets.UTF_8);
|
||||
System.out.println("4. 自定义认证:");
|
||||
System.out.println(" 原始: " + customAuth.encode());
|
||||
System.out.println(" 加密: " + customEncrypted);
|
||||
System.out.println();
|
||||
|
||||
pass();
|
||||
} catch (Exception e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 断言方法
|
||||
private static void assertEquals(Object expected, Object actual) {
|
||||
if (expected == null && actual == null) return;
|
||||
if (expected == null || !expected.equals(actual)) {
|
||||
throw new AssertionError("期望: " + expected + ", 实际: " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTrue(boolean condition) {
|
||||
if (!condition) {
|
||||
throw new AssertionError("期望为 true,实际为 false");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertFalse(boolean condition) {
|
||||
if (condition) {
|
||||
throw new AssertionError("期望为 false,实际为 true");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertNull(Object obj) {
|
||||
if (obj != null) {
|
||||
throw new AssertionError("期望为 null,实际为: " + obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pass() {
|
||||
passCount++;
|
||||
System.out.println(" ✓ 通过\n");
|
||||
}
|
||||
|
||||
private static void fail(String message) {
|
||||
failCount++;
|
||||
System.out.println(" ✗ 失败: " + message + "\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user