feat(v0.2.1): 添加认证参数支持和客户端下载命令生成

主要更新:
- 新增 auth 参数加密传递支持 (QK/UC Cookie认证)
- 实现下载命令自动生成 (curl/aria2c/迅雷)
- aria2c 命令支持 8 线程 8 片段下载
- 修复 cookie 字段映射问题
- 优化前端 clientLinks 页面
- 添加认证参数文档和测试用例
- 更新 .gitignore 忽略编译目录
This commit is contained in:
q
2026-02-05 20:35:47 +08:00
parent 97ae1a5e92
commit 6e6215ad7e
54 changed files with 6904 additions and 1471 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
/**
* 构建支持的客户端类型映射
*

View File

@@ -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;
}
}

View 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;
/**
* TokenaccessToken/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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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
# 反向代理服务器配置路径(不用加后缀)

View File

@@ -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");
}
}

View File

@@ -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");
}
}