mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-10-14 02:34:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6acb774c87 | ||
|
|
511515cae4 |
@@ -238,8 +238,6 @@ auths:
|
||||
```
|
||||
|
||||
> ⚠️ 注意:YAML 中 key 后面不写值(如 `authorization:` 空着)等同于没配置,不会生效,必须填入真实的账号密码或 token 内容。
|
||||
>
|
||||
> 若 123 云盘账号密码登录触发验证码或风控,解析会直接失败并提示改用 Authorization,不会尝试验证码。请改填上面的 `authorization` / `token`(浏览器 localStorage 的 `authorToken`,或请求头 `authorization` / Cookie `sso-token` 里的 JWT)。
|
||||
|
||||
如果只是临时调用一次,不想改动服务端配置,也可以用上面提到的 `auth` 参数临时传递(`authType` 可选 `password`/`accesstoken`/`authorization`),无需重启服务,仅本次请求生效。
|
||||
|
||||
@@ -463,7 +461,7 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
|
||||
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
|
||||
```shell
|
||||
cd ~
|
||||
wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v0.4.7/netdisk-fast-download-linux-amd64.zip
|
||||
wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v0.4.8/netdisk-fast-download-linux-amd64.zip
|
||||
unzip netdisk-fast-download.zip
|
||||
cd netdisk-fast-download
|
||||
bash service-install.sh
|
||||
|
||||
@@ -109,8 +109,6 @@ URL解码 → Base64解码 → AES解密 → JSON对象
|
||||
}
|
||||
```
|
||||
|
||||
> 123云盘(`ye`)账号密码登录如果返回验证码或风控,解析会直接失败,并提示改用 Authorization,不会尝试验证码。请在浏览器登录后复制 `authorToken` / 请求头 `authorization` / Cookie `sso-token`,用 `authType=authorization` 或 `accesstoken` 传入,或写入 `app-dev.yml` 的 `auths.ye.authorization`。
|
||||
|
||||
#### 4. 自定义认证
|
||||
```json
|
||||
{
|
||||
@@ -341,4 +339,3 @@ server:
|
||||
## 更新日志
|
||||
|
||||
- **2026-02-05**: 初始版本,支持 accesstoken、cookie、password、custom 认证类型
|
||||
- **2026-09-27**: 补充 123云盘账号密码登录触发验证码/风控时改用 Authorization 的说明
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
> 123云盘(`ye`)账号密码登录触发验证码/风控时,请改用 `authType=authorization` 或 `accesstoken`(`token` 填浏览器中的 Bearer token / JWT),或在 `app-dev.yml` 配置 `auths.ye.authorization`。解析不会尝试验证码。
|
||||
|
||||
### 自定义认证
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 账号密码登录撞上验证码/风控时的用户提示。
|
||||
* 只识别失败形态并引导改用 Authorization,不实现验证码求解、扫码或重试。
|
||||
*/
|
||||
public final class PasswordLoginRisk {
|
||||
|
||||
/**
|
||||
* 123 云盘(ye)账号密码登录被验证码/风控拦住时返回给调用方的固定说明。
|
||||
* 上游 code/message 会另附在这句话后面,便于和“密码错误”区分。
|
||||
*/
|
||||
public static final String AUTHORIZATION_HINT =
|
||||
"账号密码登录触发了验证码/风控,已停止用户名密码登录(不会尝试验证码)。"
|
||||
+ "请改为配置 Authorization(浏览器登录后的 Bearer token / JWT,"
|
||||
+ "例如 localStorage 的 authorToken、请求头 authorization 或 Cookie 的 sso-token)。"
|
||||
+ "静态配置写在 app-dev.yml 的 auths.ye.authorization(与 auths.ye.token 等价);"
|
||||
+ "临时请求使用 authType=authorization 或 authType=accesstoken,token 填该令牌(不要重复带 Bearer 前缀)。"
|
||||
+ "说明见 parser/doc/auth-param/。";
|
||||
|
||||
/**
|
||||
* 上游文案命中这些片段时视为验证码/风控,而不是单纯的密码错误。
|
||||
* 依据公开客户端(AList/OpenList)实际展示过的 123 登录失败文案,以及登录页上的滑块/人机验证提示。
|
||||
*/
|
||||
private static final String[] RISK_KEYWORDS = {
|
||||
"验证码",
|
||||
"风控",
|
||||
"安全风险",
|
||||
"安全验证",
|
||||
"安全校验",
|
||||
"滑块",
|
||||
"滑动验证",
|
||||
"拼图验证",
|
||||
"短信验证",
|
||||
"微信登录",
|
||||
"微信进行登录",
|
||||
"境外登录",
|
||||
"需要验证",
|
||||
"请验证",
|
||||
"请你验证",
|
||||
"进行验证",
|
||||
"完成验证",
|
||||
"二次验证",
|
||||
"人机验证",
|
||||
"操作频繁",
|
||||
"请求过于频繁",
|
||||
"请求频繁",
|
||||
"captcha",
|
||||
"recaptcha",
|
||||
"risk control"
|
||||
};
|
||||
|
||||
private PasswordLoginRisk() {
|
||||
}
|
||||
|
||||
public static boolean isLoginSuccess(JsonObject json) {
|
||||
if (json == null) {
|
||||
return false;
|
||||
}
|
||||
Integer code = readCode(json);
|
||||
return code != null && code == 200 && StringUtils.isNotBlank(readToken(json));
|
||||
}
|
||||
|
||||
public static boolean indicatesCaptchaOrRisk(JsonObject json) {
|
||||
if (json == null) {
|
||||
return false;
|
||||
}
|
||||
if (hasTruthyRiskField(json) || hasTruthyRiskField(dataObject(json))) {
|
||||
return true;
|
||||
}
|
||||
return textIndicatesCaptchaOrRisk(collectMessageText(json));
|
||||
}
|
||||
|
||||
public static boolean textIndicatesCaptchaOrRisk(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return false;
|
||||
}
|
||||
String lower = text.toLowerCase(Locale.ROOT);
|
||||
for (String keyword : RISK_KEYWORDS) {
|
||||
if (lower.contains(keyword.toLowerCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 非成功登录响应的用户可见说明。验证码/风控走 Authorization 引导,其余保持“登录失败: 上游文案”。
|
||||
*/
|
||||
public static String describeLoginFailure(JsonObject json) {
|
||||
if (json == null) {
|
||||
return "登录响应格式异常";
|
||||
}
|
||||
if (indicatesCaptchaOrRisk(json)) {
|
||||
return AUTHORIZATION_HINT + " 上游返回:" + upstreamSummary(json);
|
||||
}
|
||||
Integer code = readCode(json);
|
||||
if (code != null && code == 200) {
|
||||
return "未获取到token";
|
||||
}
|
||||
String message = firstMessage(json);
|
||||
if (StringUtils.isBlank(message)) {
|
||||
return "登录失败";
|
||||
}
|
||||
return "登录失败: " + message;
|
||||
}
|
||||
|
||||
public static String nonJsonLoginFailure(String body) {
|
||||
if (textIndicatesCaptchaOrRisk(body)) {
|
||||
return AUTHORIZATION_HINT + " 上游返回:" + truncate(body, 300);
|
||||
}
|
||||
return "登录响应格式异常: " + body;
|
||||
}
|
||||
|
||||
private static String collectMessageText(JsonObject json) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
appendMessage(text, json);
|
||||
JsonObject data = dataObject(json);
|
||||
if (data != null) {
|
||||
appendMessage(text, data);
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
private static void appendMessage(StringBuilder text, JsonObject json) {
|
||||
append(text, json.getString("message"));
|
||||
append(text, json.getString("msg"));
|
||||
}
|
||||
|
||||
private static void append(StringBuilder text, String value) {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
if (text.length() > 0) {
|
||||
text.append(' ');
|
||||
}
|
||||
text.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasTruthyRiskField(JsonObject obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
for (String key : obj.fieldNames()) {
|
||||
if (!isRiskFieldName(key)) {
|
||||
continue;
|
||||
}
|
||||
if (isTruthyRiskValue(obj.getValue(key))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isRiskFieldName(String key) {
|
||||
String lower = key.toLowerCase(Locale.ROOT);
|
||||
return lower.contains("captcha")
|
||||
|| lower.contains("vcode")
|
||||
|| lower.contains("risk")
|
||||
|| "needverify".equals(lower)
|
||||
|| "need_verify".equals(lower)
|
||||
|| "slideverify".equals(lower)
|
||||
|| "verifycode".equals(lower)
|
||||
|| "verify_code".equals(lower);
|
||||
}
|
||||
|
||||
private static boolean isTruthyRiskValue(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue() != 0d;
|
||||
}
|
||||
if (value instanceof CharSequence) {
|
||||
String text = value.toString().trim();
|
||||
if (text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lower = text.toLowerCase(Locale.ROOT);
|
||||
return !"false".equals(lower) && !"0".equals(lower) && !"null".equals(lower) && !"none".equals(lower);
|
||||
}
|
||||
if (value instanceof JsonObject) {
|
||||
return !((JsonObject) value).isEmpty();
|
||||
}
|
||||
if (value instanceof JsonArray) {
|
||||
return !((JsonArray) value).isEmpty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String upstreamSummary(JsonObject json) {
|
||||
StringBuilder summary = new StringBuilder();
|
||||
Integer code = readCode(json);
|
||||
if (code != null) {
|
||||
summary.append("code=").append(code);
|
||||
}
|
||||
String message = firstMessage(json);
|
||||
if (StringUtils.isNotBlank(message)) {
|
||||
if (summary.length() > 0) {
|
||||
summary.append(", ");
|
||||
}
|
||||
summary.append("message=").append(message);
|
||||
}
|
||||
if (summary.length() == 0) {
|
||||
summary.append(json.encode());
|
||||
}
|
||||
return truncate(summary.toString(), 300);
|
||||
}
|
||||
|
||||
private static String firstMessage(JsonObject json) {
|
||||
String message = json.getString("message");
|
||||
if (StringUtils.isBlank(message)) {
|
||||
message = json.getString("msg");
|
||||
}
|
||||
if (StringUtils.isBlank(message)) {
|
||||
JsonObject data = dataObject(json);
|
||||
if (data != null) {
|
||||
message = data.getString("message");
|
||||
if (StringUtils.isBlank(message)) {
|
||||
message = data.getString("msg");
|
||||
}
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
static String readToken(JsonObject json) {
|
||||
JsonObject data = dataObject(json);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
Object token = data.getValue("token");
|
||||
return token == null ? null : token.toString();
|
||||
}
|
||||
|
||||
private static JsonObject dataObject(JsonObject json) {
|
||||
Object data = json.getValue("data");
|
||||
return data instanceof JsonObject ? (JsonObject) data : null;
|
||||
}
|
||||
|
||||
static Integer readCode(JsonObject json) {
|
||||
Object code = json.getValue("code");
|
||||
if (code instanceof Number) {
|
||||
return ((Number) code).intValue();
|
||||
}
|
||||
if (code instanceof String && StringUtils.isNumeric(((String) code).trim())) {
|
||||
return Integer.parseInt(((String) code).trim());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String truncate(String value, int max) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
if (trimmed.length() <= max) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.substring(0, max) + "...";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package cn.qaiu.parser.impl;
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.parser.TokenCache;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import cn.qaiu.util.AcwScV2Generator;
|
||||
import cn.qaiu.util.CommonUtils;
|
||||
@@ -85,12 +86,31 @@ public class IzTool extends PanBase {
|
||||
}
|
||||
public IzTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
IzXffHeader.apply(header);
|
||||
String accountId = "_default";
|
||||
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||
if (auths.contains("_configId")) {
|
||||
accountId = auths.get("_configId");
|
||||
} else if (auths.contains("username")) {
|
||||
accountId = auths.get("username");
|
||||
}
|
||||
}
|
||||
this.cacheKey = TokenCache.key("iz", accountId);
|
||||
}
|
||||
|
||||
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||
|
||||
public static volatile String token = null;
|
||||
public static volatile boolean authFlag = true;
|
||||
// 多账号隔离:用 TokenCache 代替 static 变量
|
||||
private final String cacheKey;
|
||||
|
||||
private String token() {
|
||||
return TokenCache.getToken(cacheKey);
|
||||
}
|
||||
|
||||
private boolean authFlag() {
|
||||
return TokenCache.getFlag(cacheKey, true);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
|
||||
@@ -101,22 +121,21 @@ public class IzTool extends PanBase {
|
||||
// 检查并输出认证状态
|
||||
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||
String token = token();
|
||||
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
|
||||
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(8, token.length())) + "...)" : "未登录");
|
||||
isTempAuth, authFlag(), token != null ? "已登录(" + token.substring(0, Math.min(8, token.length())) + "...)" : "未登录");
|
||||
|
||||
// 如果需要认证但还没有token,先执行登录
|
||||
if ((isTempAuth || authFlag) && token == null) {
|
||||
if ((isTempAuth || authFlag()) && token == null) {
|
||||
log.info("文件解析需要登录,开始执行登录流程...");
|
||||
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||
return login(tsEncode, auths)
|
||||
.compose(v -> {
|
||||
log.info("文件解析预登录成功,继续解析流程");
|
||||
return parseWithAuth(shareId, tsEncode);
|
||||
.recover(err -> {
|
||||
// 预登录失败不终止解析,继续走免登录
|
||||
log.warn("文件解析预登录失败: {},使用免登录模式", err.getMessage());
|
||||
return Future.succeededFuture();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.warn("文件解析预登录失败: {},尝试使用免登录模式", err.getMessage());
|
||||
// 登录失败,继续使用免登录模式
|
||||
});
|
||||
.compose(v -> parseWithAuth(shareId, tsEncode));
|
||||
} else if (token != null) {
|
||||
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(8, token.length())));
|
||||
}
|
||||
@@ -231,8 +250,8 @@ public class IzTool extends PanBase {
|
||||
// 检查是否为临时认证(临时认证每次都尝试登录)
|
||||
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
|
||||
// 如果是临时认证,或者是后台配置且authFlag为true,则尝试使用认证
|
||||
if (isTempAuth || authFlag) {
|
||||
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
|
||||
if (isTempAuth || authFlag()) {
|
||||
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag());
|
||||
HttpRequest<Buffer> httpRequest =
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
@@ -241,18 +260,19 @@ public class IzTool extends PanBase {
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey);
|
||||
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||
if (token == null) {
|
||||
// 执行登录
|
||||
if (token() == null) {
|
||||
// 执行登录,失败则回退免登录
|
||||
login(tsEncode2, auths).onFailure(failRes-> {
|
||||
log.warn("登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
log.warn("登录失败,回退免登录: {}", failRes.getMessage());
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
httpRequest.setTemplateParam("appToken", token())
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
} else {
|
||||
// 验证token
|
||||
// 验证token — 先从缓存恢复到 header
|
||||
header.set("appToken", token());
|
||||
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
@@ -260,15 +280,15 @@ public class IzTool extends PanBase {
|
||||
// log.info("res: {}",asJson(res));
|
||||
if (asJson(res).getInteger("code") != 200) {
|
||||
login(tsEncode2, auths).onFailure(failRes -> {
|
||||
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
log.warn("重新登录失败,回退免登录: {}", failRes.getMessage());
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
httpRequest.setTemplateParam("appToken", token())
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
});
|
||||
} else {
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
httpRequest.setTemplateParam("appToken", token())
|
||||
.putHeaders(header);
|
||||
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
}
|
||||
@@ -277,29 +297,27 @@ public class IzTool extends PanBase {
|
||||
} else {
|
||||
// authFlag 为 false,使用免登录解析
|
||||
log.debug("authFlag=false,使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}
|
||||
} else {
|
||||
// 没有认证信息,使用免登录解析
|
||||
log.debug("无认证信息,使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadAnonymous(String fidEncode, String tsEncode2, String auth, String dataKey) {
|
||||
log.debug("使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
}
|
||||
|
||||
private Future<Void> login(String tsEncode2, MultiMap auths) {
|
||||
Promise<Void> promise1 = Promise.promise();
|
||||
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
|
||||
@@ -310,8 +328,13 @@ public class IzTool extends PanBase {
|
||||
.onSuccess(res2->{
|
||||
JsonObject json = asJson(res2);
|
||||
if (json.getInteger("code") == 200) {
|
||||
token = json.getJsonObject("data").getString("appToken");
|
||||
log.info("登录成功 token: {}...", token != null ? token.substring(0, Math.min(8, token.length())) : "null");
|
||||
String loginToken = json.getJsonObject("data").getString("appToken");
|
||||
TokenCache.putToken(cacheKey, loginToken);
|
||||
TokenCache.queueCachedTokenPersist(cacheKey, loginToken, System.currentTimeMillis() + 24 * 60 * 60 * 1000L);
|
||||
if (loginToken != null) {
|
||||
header.set("appToken", loginToken);
|
||||
}
|
||||
log.info("登录成功 token: {}...", loginToken != null ? loginToken.substring(0, Math.min(8, loginToken.length())) : "null");
|
||||
promise1.complete();
|
||||
} else {
|
||||
// 检查是否为临时认证
|
||||
@@ -323,7 +346,7 @@ public class IzTool extends PanBase {
|
||||
} else {
|
||||
// 后台配置的认证失败,设置authFlag并返回失败,让下次请求使用免登陆解析
|
||||
log.warn("后台配置认证失败: {}, authFlag将设为false,请重新解析", json.getString("msg"));
|
||||
authFlag = false;
|
||||
TokenCache.putFlag(cacheKey, false);
|
||||
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
|
||||
}
|
||||
}
|
||||
@@ -443,75 +466,75 @@ public class IzTool extends PanBase {
|
||||
}
|
||||
}
|
||||
|
||||
private void down(HttpResponse<Buffer> res2) {
|
||||
MultiMap headers = res2.headers();
|
||||
String location = headers.get("Location");
|
||||
if (StringUtils.isBlank(location)) {
|
||||
fail("{}", buildMissingLocationMessage(res2));
|
||||
return;
|
||||
}
|
||||
promise.complete(location);
|
||||
}
|
||||
|
||||
private String buildMissingLocationMessage(HttpResponse<Buffer> response) {
|
||||
StringBuilder message = new StringBuilder("未获取到下载重定向地址");
|
||||
message.append(", HTTP ").append(response.statusCode());
|
||||
|
||||
String body = null;
|
||||
try {
|
||||
body = asText(response);
|
||||
} catch (Exception e) {
|
||||
body = "<响应体读取失败: " + e.getMessage() + ">";
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(body)) {
|
||||
try {
|
||||
JsonObject json = new JsonObject(body);
|
||||
String upstreamMsg = json.getString("msg");
|
||||
Object code = json.getValue("code");
|
||||
if (StringUtils.isNotBlank(upstreamMsg)) {
|
||||
message.append(", 上游返回: ").append(upstreamMsg);
|
||||
if (code != null) {
|
||||
message.append(" (code=").append(code).append(")");
|
||||
}
|
||||
} else {
|
||||
message.append(", 响应体: ").append(previewBody(body));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
message.append(", 响应体: ").append(previewBody(body));
|
||||
}
|
||||
} else {
|
||||
message.append(", 响应体为空");
|
||||
}
|
||||
|
||||
Object fileName = shareLinkInfo.getOtherParam().get("fileName");
|
||||
Object fileSize = shareLinkInfo.getOtherParam().get("fileSizeFormat");
|
||||
if (fileName != null) {
|
||||
message.append(", 文件: ").append(fileName);
|
||||
}
|
||||
if (fileSize != null) {
|
||||
message.append(", 大小: ").append(fileSize);
|
||||
}
|
||||
if (!hasConfiguredAuth()) {
|
||||
message.append(", 当前为免登录解析,上游可能要求登录、会员或人工处理");
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private boolean hasConfiguredAuth() {
|
||||
Object authObj = shareLinkInfo.getOtherParam().get("auths");
|
||||
if (!(authObj instanceof MultiMap auths)) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.isNotBlank(auths.get("username")) && StringUtils.isNotBlank(auths.get("password"));
|
||||
}
|
||||
|
||||
private String previewBody(String body) {
|
||||
int maxLength = 500;
|
||||
return body.length() <= maxLength ? body : body.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
// 目录解析
|
||||
private void down(HttpResponse<Buffer> res2) {
|
||||
MultiMap headers = res2.headers();
|
||||
String location = headers.get("Location");
|
||||
if (StringUtils.isBlank(location)) {
|
||||
fail("{}", buildMissingLocationMessage(res2));
|
||||
return;
|
||||
}
|
||||
promise.complete(location);
|
||||
}
|
||||
|
||||
private String buildMissingLocationMessage(HttpResponse<Buffer> response) {
|
||||
StringBuilder message = new StringBuilder("未获取到下载重定向地址");
|
||||
message.append(", HTTP ").append(response.statusCode());
|
||||
|
||||
String body = null;
|
||||
try {
|
||||
body = asText(response);
|
||||
} catch (Exception e) {
|
||||
body = "<响应体读取失败: " + e.getMessage() + ">";
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(body)) {
|
||||
try {
|
||||
JsonObject json = new JsonObject(body);
|
||||
String upstreamMsg = json.getString("msg");
|
||||
Object code = json.getValue("code");
|
||||
if (StringUtils.isNotBlank(upstreamMsg)) {
|
||||
message.append(", 上游返回: ").append(upstreamMsg);
|
||||
if (code != null) {
|
||||
message.append(" (code=").append(code).append(")");
|
||||
}
|
||||
} else {
|
||||
message.append(", 响应体: ").append(previewBody(body));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
message.append(", 响应体: ").append(previewBody(body));
|
||||
}
|
||||
} else {
|
||||
message.append(", 响应体为空");
|
||||
}
|
||||
|
||||
Object fileName = shareLinkInfo.getOtherParam().get("fileName");
|
||||
Object fileSize = shareLinkInfo.getOtherParam().get("fileSizeFormat");
|
||||
if (fileName != null) {
|
||||
message.append(", 文件: ").append(fileName);
|
||||
}
|
||||
if (fileSize != null) {
|
||||
message.append(", 大小: ").append(fileSize);
|
||||
}
|
||||
if (!hasConfiguredAuth()) {
|
||||
message.append(", 当前为免登录解析,上游可能要求登录、会员或人工处理");
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private boolean hasConfiguredAuth() {
|
||||
Object authObj = shareLinkInfo.getOtherParam().get("auths");
|
||||
if (!(authObj instanceof MultiMap auths)) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.isNotBlank(auths.get("username")) && StringUtils.isNotBlank(auths.get("password"));
|
||||
}
|
||||
|
||||
private String previewBody(String body) {
|
||||
int maxLength = 500;
|
||||
return body.length() <= maxLength ? body : body.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
// 目录解析
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
@@ -691,7 +714,6 @@ public class IzTool extends PanBase {
|
||||
}
|
||||
|
||||
public static void resetToken() {
|
||||
token = null;
|
||||
authFlag = true;
|
||||
TokenCache.removeByDiskType("iz");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public class IzToolWithAuth extends PanBase {
|
||||
}
|
||||
public IzToolWithAuth(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
IzXffHeader.apply(header);
|
||||
}
|
||||
|
||||
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
|
||||
@@ -213,8 +214,8 @@ public class IzToolWithAuth extends PanBase {
|
||||
if (token == null) {
|
||||
// 执行登录
|
||||
login(tsEncode2, auths).onFailure(failRes-> {
|
||||
log.warn("登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
log.warn("登录失败,回退免登录: {}", failRes.getMessage());
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
@@ -229,8 +230,8 @@ public class IzToolWithAuth extends PanBase {
|
||||
// log.info("res: {}",asJson(res));
|
||||
if (asJson(res).getInteger("code") != 200) {
|
||||
login(tsEncode2, auths).onFailure(failRes -> {
|
||||
log.warn("重新登录失败: {}", failRes.getMessage());
|
||||
fail(failRes.getMessage());
|
||||
log.warn("重新登录失败,回退免登录: {}", failRes.getMessage());
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}).onSuccess(r-> {
|
||||
httpRequest.setTemplateParam("appToken", token)
|
||||
.putHeaders(header);
|
||||
@@ -246,29 +247,27 @@ public class IzToolWithAuth extends PanBase {
|
||||
} else {
|
||||
// authFlag 为 false,使用免登录解析
|
||||
log.debug("authFlag=false,使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}
|
||||
} else {
|
||||
// 没有认证信息,使用免登录解析
|
||||
log.debug("无认证信息,使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
downloadAnonymous(fidEncode, tsEncode2, auth, dataKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadAnonymous(String fidEncode, String tsEncode2, String auth, String dataKey) {
|
||||
log.debug("使用免登录解析");
|
||||
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey).send()
|
||||
.onSuccess(this::down).onFailure(handleFail("请求2"));
|
||||
}
|
||||
|
||||
private Future<Void> login(String tsEncode2, MultiMap auths) {
|
||||
Promise<Void> promise1 = Promise.promise();
|
||||
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 蓝奏云优享 X-Forwarded-For 配置。
|
||||
* <p>
|
||||
* 读取系统属性 {@code nfd.iz.xff},其次环境变量 {@code NFD_IZ_XFF}:
|
||||
* <ul>
|
||||
* <li>未设置 / 空 / {@code off}:不发送该头(默认)</li>
|
||||
* <li>{@code random}:每次创建解析器时随机一个国内常见网段 IP</li>
|
||||
* <li>其它值:原样作为头的值,例如 {@code 113.96.181.18}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class IzXffHeader {
|
||||
|
||||
public static final String PROP = "nfd.iz.xff";
|
||||
public static final String ENV = "NFD_IZ_XFF";
|
||||
private static final String HEADER = "X-Forwarded-For";
|
||||
|
||||
/** 国内运营商常见 /8 段首字节 */
|
||||
private static final int[] CN_PREFIX = {36, 39, 42, 58, 59, 60, 61, 101, 106, 110, 111, 112, 113, 114,
|
||||
115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 175, 180, 182, 183, 202, 210, 211, 218, 219, 220, 221, 222, 223};
|
||||
|
||||
private IzXffHeader() {
|
||||
}
|
||||
|
||||
static String configured() {
|
||||
String v = System.getProperty(PROP);
|
||||
if (StringUtils.isBlank(v)) {
|
||||
v = System.getenv(ENV);
|
||||
}
|
||||
return v == null ? "" : v.trim();
|
||||
}
|
||||
|
||||
/** 根据配置得到头的值,返回 null 表示不发送 */
|
||||
static String resolve(String mode) {
|
||||
if (StringUtils.isBlank(mode) || "off".equalsIgnoreCase(mode)) {
|
||||
return null;
|
||||
}
|
||||
if ("random".equalsIgnoreCase(mode)) {
|
||||
ThreadLocalRandom r = ThreadLocalRandom.current();
|
||||
return CN_PREFIX[r.nextInt(CN_PREFIX.length)] + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + (1 + r.nextInt(254));
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
/** 按当前配置写入或移除请求头 */
|
||||
public static void apply(MultiMap headers) {
|
||||
String value = resolve(configured());
|
||||
if (value == null) {
|
||||
headers.remove(HEADER);
|
||||
} else {
|
||||
headers.set(HEADER, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package cn.qaiu.parser.impl;
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.parser.PasswordLoginRisk;
|
||||
import cn.qaiu.parser.TokenCache;
|
||||
import cn.qaiu.util.CommonUtils;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
@@ -273,27 +272,24 @@ public class Ye2Tool extends PanBase {
|
||||
.putHeader("platform", "web")
|
||||
.sendJsonObject(loginBody)
|
||||
.onSuccess(res -> {
|
||||
JsonObject json;
|
||||
try {
|
||||
json = res.bodyAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
promise.fail(PasswordLoginRisk.nonJsonLoginFailure(res.bodyAsString()));
|
||||
return;
|
||||
}
|
||||
JsonObject json = res.bodyAsJsonObject();
|
||||
if (json == null) {
|
||||
promise.fail(PasswordLoginRisk.nonJsonLoginFailure(res.bodyAsString()));
|
||||
promise.fail("登录响应格式异常: " + res.bodyAsString());
|
||||
return;
|
||||
}
|
||||
if (!json.containsKey("code")) {
|
||||
promise.fail("登录响应格式异常: " + res.bodyAsString());
|
||||
return;
|
||||
}
|
||||
// 验证码/风控只提示改用 Authorization,不求解、不重试。
|
||||
if (!PasswordLoginRisk.isLoginSuccess(json)) {
|
||||
promise.fail(PasswordLoginRisk.describeLoginFailure(json));
|
||||
if (json.getInteger("code") != 200) {
|
||||
promise.fail("登录失败: " + json.getString("message"));
|
||||
return;
|
||||
}
|
||||
JsonObject data = json.getJsonObject("data");
|
||||
if (data == null || !data.containsKey("token")) {
|
||||
promise.fail("未获取到token");
|
||||
return;
|
||||
}
|
||||
String ssoToken = data.getString("token");
|
||||
String expireStr = data.getString("expire");
|
||||
long expireMs;
|
||||
|
||||
@@ -55,50 +55,8 @@ def encode123(url, way, version, timestamp):
|
||||
# 返回加密后的URL参数
|
||||
return f"?{y}={time_long}-{a}-{final_crc}"
|
||||
|
||||
# 与 parser PasswordLoginRisk 保持一致:验证码/风控只提示改用 Authorization,不求解、不重试。
|
||||
_RISK_KEYWORDS = (
|
||||
"验证码", "风控", "安全风险", "安全验证", "安全校验", "滑块", "滑动验证",
|
||||
"拼图验证", "短信验证", "微信登录", "微信进行登录", "境外登录", "需要验证", "请验证",
|
||||
"请你验证", "进行验证", "完成验证", "二次验证", "人机验证", "操作频繁", "请求过于频繁",
|
||||
"请求频繁", "captcha", "recaptcha", "risk control",
|
||||
)
|
||||
_AUTH_HINT = (
|
||||
"账号密码登录触发了验证码/风控,已停止用户名密码登录(不会尝试验证码)。"
|
||||
"请改为配置 Authorization(浏览器登录后的 Bearer token / JWT,"
|
||||
"例如 localStorage 的 authorToken、请求头 authorization 或 Cookie 的 sso-token)。"
|
||||
"静态配置写在 app-dev.yml 的 auths.ye.authorization(与 auths.ye.token 等价);"
|
||||
"临时请求使用 authType=authorization 或 authType=accesstoken,token 填该令牌(不要重复带 Bearer 前缀)。"
|
||||
"说明见 parser/doc/auth-param/。"
|
||||
)
|
||||
|
||||
|
||||
def format_login_failure(result):
|
||||
"""把 123 登录失败响应转成用户可见说明。"""
|
||||
if not isinstance(result, dict):
|
||||
return "登录失败"
|
||||
message = result.get("message") or result.get("msg") or ""
|
||||
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
||||
data_message = data.get("message") or data.get("msg") or ""
|
||||
blob = f"{message} {data_message}".lower()
|
||||
risk_field = any(
|
||||
key.lower().find(flag) >= 0 and data.get(key) not in (None, False, 0, "", "false", "0")
|
||||
for key in data
|
||||
for flag in ("captcha", "vcode", "risk", "needverify", "need_verify", "slideverify", "verifycode", "verify_code")
|
||||
)
|
||||
if risk_field or any(keyword.lower() in blob for keyword in _RISK_KEYWORDS):
|
||||
code = result.get("code")
|
||||
upstream = message or data_message
|
||||
extra = f"code={code}" if code is not None else ""
|
||||
if upstream:
|
||||
extra = f"{extra}, message={upstream}" if extra else f"message={upstream}"
|
||||
return f"{_AUTH_HINT} 上游返回:{extra}" if extra else _AUTH_HINT
|
||||
if result.get("code") == 200:
|
||||
return "未获取到token"
|
||||
return f"登录失败: {message}" if message else "登录失败"
|
||||
|
||||
|
||||
def login_123pan(username, password):
|
||||
"""登录123盘获取token。成功返回 token,失败返回 None(错误已打印)。"""
|
||||
"""登录123盘获取token"""
|
||||
print(f"🔐 正在登录账号: {username}")
|
||||
|
||||
login_data = {
|
||||
@@ -115,18 +73,16 @@ def login_123pan(username, password):
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
if result.get('code') == 200 and result.get('data', {}).get('token'):
|
||||
if result.get('code') == 200:
|
||||
token = result.get('data', {}).get('token', '')
|
||||
print(f"✅ 登录成功!")
|
||||
return token
|
||||
else:
|
||||
error_msg = format_login_failure(result)
|
||||
login_123pan.last_error = error_msg
|
||||
print(f"❌ {error_msg}")
|
||||
error_msg = result.get('message', '未知错误')
|
||||
print(f"❌ 登录失败: {error_msg}")
|
||||
return None
|
||||
except Exception as e:
|
||||
login_123pan.last_error = f"登录请求失败: {e}"
|
||||
print(f"❌ {login_123pan.last_error}")
|
||||
print(f"❌ 登录请求失败: {e}")
|
||||
return None
|
||||
|
||||
def get_share_info(share_key, password=''):
|
||||
@@ -233,7 +189,7 @@ def start(link, password='', username='', user_password=''):
|
||||
if not token:
|
||||
return {
|
||||
"code": 201,
|
||||
"message": getattr(login_123pan, "last_error", None) or "登录失败"
|
||||
"message": "登录失败"
|
||||
}
|
||||
else:
|
||||
print("⚠️ 未提供登录信息,某些文件可能无法下载")
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PasswordLoginRiskTest {
|
||||
|
||||
@Test
|
||||
public void securityRiskMessageGuidesAuthorization() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 401)
|
||||
.put("message", "当前账号存在安全风险,请使用短信验证码或者微信进行登录。");
|
||||
|
||||
assertTrue(PasswordLoginRisk.indicatesCaptchaOrRisk(body));
|
||||
String message = PasswordLoginRisk.describeLoginFailure(body);
|
||||
assertTrue(message.startsWith(PasswordLoginRisk.AUTHORIZATION_HINT));
|
||||
assertTrue(message.contains("验证码/风控"));
|
||||
assertTrue(message.contains("auths.ye.authorization"));
|
||||
assertTrue(message.contains("authType=authorization"));
|
||||
assertTrue(message.contains("authType=accesstoken"));
|
||||
assertTrue(message.contains("code=401"));
|
||||
assertTrue(message.contains("当前账号存在安全风险"));
|
||||
assertFalse(message.startsWith("登录失败:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overseasRiskAndCaptchaPhrasesMatch() {
|
||||
assertRisk("当前账号存在境外登录风险,请使用短信验证码或者微信进行登录。");
|
||||
assertRisk("请完成安全验证");
|
||||
assertRisk("请进行滑块验证");
|
||||
assertRisk("操作频繁,请稍后再试");
|
||||
assertRisk("captcha required");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrongPasswordStaysDistinct() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 500)
|
||||
.put("message", "用户名或密码错误");
|
||||
|
||||
assertFalse(PasswordLoginRisk.indicatesCaptchaOrRisk(body));
|
||||
assertEquals("登录失败: 用户名或密码错误", PasswordLoginRisk.describeLoginFailure(body));
|
||||
|
||||
JsonObject account = new JsonObject().put("code", 1).put("msg", "账号或密码错误");
|
||||
assertFalse(PasswordLoginRisk.indicatesCaptchaOrRisk(account));
|
||||
assertEquals("登录失败: 账号或密码错误", PasswordLoginRisk.describeLoginFailure(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riskFieldWithoutKeywordStillHints() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 200)
|
||||
.put("message", "ok")
|
||||
.put("data", new JsonObject().put("needCaptcha", true));
|
||||
|
||||
assertFalse(PasswordLoginRisk.isLoginSuccess(body));
|
||||
assertTrue(PasswordLoginRisk.indicatesCaptchaOrRisk(body));
|
||||
assertTrue(PasswordLoginRisk.describeLoginFailure(body).contains("Authorization"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void falseCaptchaFlagDoesNotHint() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 500)
|
||||
.put("message", "用户名或密码错误")
|
||||
.put("data", new JsonObject().put("needCaptcha", false));
|
||||
|
||||
assertFalse(PasswordLoginRisk.indicatesCaptchaOrRisk(body));
|
||||
assertEquals("登录失败: 用户名或密码错误", PasswordLoginRisk.describeLoginFailure(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successTokenIsNotTreatedAsFailure() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 200)
|
||||
.put("message", "ok")
|
||||
.put("data", new JsonObject().put("token", "jwt-token"));
|
||||
|
||||
assertTrue(PasswordLoginRisk.isLoginSuccess(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankTokenWithoutRiskKeepsMissingTokenMessage() {
|
||||
JsonObject body = new JsonObject()
|
||||
.put("code", 200)
|
||||
.put("message", "ok")
|
||||
.put("data", new JsonObject().put("token", ""));
|
||||
|
||||
assertFalse(PasswordLoginRisk.isLoginSuccess(body));
|
||||
assertEquals("未获取到token", PasswordLoginRisk.describeLoginFailure(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonJsonCaptchaBodyUsesHint() {
|
||||
String body = "<html>请完成滑块验证</html>";
|
||||
String message = PasswordLoginRisk.nonJsonLoginFailure(body);
|
||||
assertTrue(message.startsWith(PasswordLoginRisk.AUTHORIZATION_HINT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonJsonWrongPasswordStaysFormatError() {
|
||||
assertEquals("登录响应格式异常: 用户名或密码错误",
|
||||
PasswordLoginRisk.nonJsonLoginFailure("用户名或密码错误"));
|
||||
}
|
||||
|
||||
private static void assertRisk(String message) {
|
||||
JsonObject body = new JsonObject().put("code", 5202).put("message", message);
|
||||
assertTrue(message, PasswordLoginRisk.indicatesCaptchaOrRisk(body));
|
||||
assertTrue(PasswordLoginRisk.describeLoginFailure(body).startsWith(PasswordLoginRisk.AUTHORIZATION_HINT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class IzXffHeaderTest {
|
||||
@Test
|
||||
public void offByDefault() {
|
||||
assertNull(IzXffHeader.resolve(null));
|
||||
assertNull(IzXffHeader.resolve(""));
|
||||
assertNull(IzXffHeader.resolve("OFF"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void literalAndRandom() {
|
||||
assertEquals("113.96.181.18", IzXffHeader.resolve("113.96.181.18"));
|
||||
String ip = IzXffHeader.resolve("random");
|
||||
assertTrue(ip, ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyRemovesWhenOff() {
|
||||
MultiMap h = MultiMap.caseInsensitiveMultiMap().set("X-Forwarded-For", "1.1.1.1");
|
||||
System.clearProperty(IzXffHeader.PROP);
|
||||
if (System.getenv(IzXffHeader.ENV) == null) {
|
||||
IzXffHeader.apply(h);
|
||||
assertFalse(h.contains("X-Forwarded-For"));
|
||||
}
|
||||
System.setProperty(IzXffHeader.PROP, "8.8.8.8");
|
||||
IzXffHeader.apply(h);
|
||||
assertEquals("8.8.8.8", h.get("X-Forwarded-For"));
|
||||
System.clearProperty(IzXffHeader.PROP);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.4.7</revision>
|
||||
<revision>0.4.8</revision>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nfd-web",
|
||||
"version": "0.4.7",
|
||||
"version": "0.4.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
||||
Reference in New Issue
Block a user