Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1de6f2186f | |||
| a198fc9d81 | |||
| 24caa18883 | |||
| 41c2771ed8 | |||
| 871d92daa4 | |||
| fa9a0ea04e | |||
| 26a64b44e9 | |||
| df745e6f83 | |||
| 687c5a62d2 | |||
| 0b44bab651 | |||
| db92f7c0ad | |||
| 5248ed41bd | |||
| 323e19a63c | |||
| 91ddcc8e7b |
@@ -20,6 +20,10 @@ java -cp "runtime:libs/*" Main
|
||||
应用程序通过 ./config/config.ini 文件配置应用运行参数,日志级别与日志路径。
|
||||
应用程序配置文件兜底路径为 /sdcard/WinBoLLStudio/AuthCenterConsoleApp/config/config.ini 。
|
||||
|
||||
Docker环境配置简要
|
||||
需要挂载config.ini配置文件的apks_folder_path的路径。
|
||||
[APP]
|
||||
apks_folder_path=/sdcard/WinBoLLStudio/APKs
|
||||
|
||||
公网访问接口
|
||||
https://console.winboll.cc/authcenter/ping
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
# 关闭 errexit,改用手动判断关键步骤,避免非致命错误中断
|
||||
set -uo pipefail
|
||||
|
||||
# 前置校验:当前是否为git仓库,非git仓库直接退出
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
@@ -7,26 +8,34 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 重置本地修改
|
||||
git restore .
|
||||
# 重置本地修改(无修改也不报错,正常继续)
|
||||
echo "重置本地修改..."
|
||||
git restore . >/dev/null 2>&1
|
||||
|
||||
# 切main分支:屏蔽正常提示,仅失败时退出
|
||||
# 切main分支:无论是否已在main,都正常继续,仅真失败才退出
|
||||
echo "切换到main分支..."
|
||||
git checkout main >/dev/null 2>&1 || { echo "切换main分支失败,终止执行"; exit 1; }
|
||||
|
||||
# 拉最新代码:兼容"已最新"场景,仅真失败才退出(核心修复)
|
||||
echo "拉取main分支最新代码..."
|
||||
if ! git pull origin main --rebase >/dev/null 2>&1; then
|
||||
echo "拉取main分支最新代码失败,终止执行"
|
||||
git checkout main >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:切换main分支失败,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 优先取HEAD关联tag,无则取仓库最近tag
|
||||
# 拉取main分支最新代码+远程所有tag(核心:同步最新代码和tag)
|
||||
echo "拉取main分支最新代码..."
|
||||
git pull origin main --rebase >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:拉取main分支最新代码失败,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 拉取远程所有tag,确保本地有最新tag(关键补充)
|
||||
echo "同步远程最新tag..."
|
||||
git fetch origin --tags >/dev/null 2>&1
|
||||
|
||||
# 【核心修复】直接获取仓库最新tag(按提交时间排序,删除HEAD关联tag的优先逻辑)
|
||||
get_latest_tag() {
|
||||
local head_tag=$(git log -1 --pretty=format:%D 2>/dev/null | grep -o 'tag: [^, ]*' | awk -F': ' '{print $2}')
|
||||
[ -n "$head_tag" ] && { echo "$head_tag"; return 0; }
|
||||
|
||||
local latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
||||
# 按提交时间倒序取最新tag,兼容v1.0.8、v1.0.9命名格式
|
||||
local latest_tag=$(git tag -l | sort -V | tail -n1 2>/dev/null || true)
|
||||
[ -n "$latest_tag" ] && { echo "$latest_tag"; return 0; }
|
||||
|
||||
echo "no_tag" && return 1
|
||||
@@ -35,19 +44,29 @@ get_latest_tag() {
|
||||
# 获取并校验tag,无tag直接退出
|
||||
latest_tag=$(get_latest_tag)
|
||||
if [ "$latest_tag" = "no_tag" ]; then
|
||||
echo "仓库无任何tag,终止执行"
|
||||
echo "错误:仓库无任何tag,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
echo "获取到目标tag:$latest_tag"
|
||||
echo "获取到最新目标tag:$latest_tag"
|
||||
|
||||
# 切换tag,失败退出
|
||||
git checkout "$latest_tag" || { echo "切换至tag $latest_tag失败,终止执行"; exit 1; }
|
||||
echo "切换至tag $latest_tag..."
|
||||
git checkout "$latest_tag" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:切换至tag $latest_tag失败,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行构建脚本,失败直接退出
|
||||
bash bash/build_class.sh || { echo "构建脚本执行失败,终止执行"; exit 1; }
|
||||
echo "执行构建脚本..."
|
||||
bash bash/build_class.sh
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:构建脚本执行失败,终止执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 确保config目录存在,写入tag信息
|
||||
mkdir -p config
|
||||
mkdir -p config >/dev/null 2>&1
|
||||
echo "$latest_tag" > config/version.flags
|
||||
echo "✅ 全部操作完成!tag已写入config/version.flags"
|
||||
echo "✅ 全部操作完成!最新 tag[$latest_tag] 已写入 config/version.flags 。"
|
||||
|
||||
|
||||
@@ -1,42 +1,68 @@
|
||||
;AuthCenter 配置文件(INI格式,适配smtp_auth_code读取校验)
|
||||
; 说明:已匹配日志校验键名,替换为占位符说明,填真实值可直接启动,分号开头为注释
|
||||
; 说明:已匹配日志校验键名,按注释提示填写后可直接启动,分号开头为注释,等号前后无空格
|
||||
; 配置规则:路径支持绝对路径,数值型参数为整型,时间单位统一为毫秒(timeout/interval类)
|
||||
[GlobalConfig]
|
||||
log_level=【必填|可选值:ALL/FINE/INFO/WARNING/SEVERE】日志输出级别,默认填INFO
|
||||
service_heartbeat_interval=【必填|正整数】服务心跳检测间隔(毫秒),默认填500
|
||||
|
||||
; 项目根目录(绝对路径,需指向AuthCenterConsoleApp根目录,建议与部署路径一致)
|
||||
root_path=/sdcard/ZhanGSKen/Sources/AuthCenterConsoleApp
|
||||
; 日志级别:可选 ALL/FINE/INFO/WARNING/SEVERE,开发环境用ALL,生产环境用INFO/WARNING
|
||||
log_level=ALL
|
||||
; 日志文件存储目录(基于root_path的相对路径,自动创建)
|
||||
log_path=logs
|
||||
; RSA密钥对存储目录(基于root_path的相对路径,自动创建,用于加解密)
|
||||
rsakeys_path=rsakeys
|
||||
; 报告文件存储目录(基于root_path的相对路径,自动创建,如校验报告/运行报告)
|
||||
reports_path=reports
|
||||
; 服务心跳检测间隔(单位:毫秒,建议500-2000,数值越小检测越频繁)
|
||||
service_heartbeat_interval=500
|
||||
|
||||
[VersionConfig]
|
||||
; 版本号文件存储路径(基于root_path的相对路径,由构建脚本自动写入tag信息)
|
||||
version_file_path=config/version.flags
|
||||
; 版本号合法正则表达式(匹配v+主版本+次版本+修订版,如v1.0.9,请勿随意修改)
|
||||
version_valid_regex=^v\d+.\d+.\d+$
|
||||
|
||||
[APP]
|
||||
; APK文件根目录(绝对路径,用于存储待校验的APK安装包,自动按项目名/版本名分目录)
|
||||
apks_folder_path=/sdcard/WinBoLLStudio/APKs
|
||||
|
||||
[EmailConfig]
|
||||
; 邮件SMTP服务器(QQ邮箱填smtp.qq.com,163填smtp.163.com,按需修改)
|
||||
smtp_host=【必填】SMTP服务器地址,QQ邮箱固定smtp.qq.com
|
||||
; SSL端口465,兼容性强,无需改;非SSL填25
|
||||
smtp_port=【必填|端口号】邮件服务端口,推荐填465(SSL)
|
||||
; 发送方邮箱(需和smtp_auth_code对应)
|
||||
send_email_account=【必填】发送通知的邮箱账号(如xxx@qq.com)
|
||||
; 核心:smtp_auth_code,QQ邮箱填授权码(非登录密码),其他邮箱填对应授权码/密码
|
||||
smtp_auth_code=【必填】邮箱SMTP授权码,QQ邮箱需去官网开启SMTP后获取
|
||||
; 发件人昵称(收件方看到的发件人名称)
|
||||
from_nickname=【可选】发件人显示昵称,如WinBoLLStudio
|
||||
smtp_connect_timeout=【可选|正整数】邮件连接超时时间(毫秒),默认填5000
|
||||
email_subject_prefix=【可选】邮件标题前缀,如WinBoLLStudio
|
||||
verify_code_email_content=【可选】验证码邮件内容,{code}为验证码占位符,请勿删除
|
||||
|
||||
[ServerConfig]
|
||||
server_base_url=【必填】服务基础地址,本地调试填http://localhost:端口号(和http_server_port一致)
|
||||
|
||||
; 邮件SMTP服务器地址(QQ邮箱固定为smtp.qq.com,163邮箱为smtp.163.com,无需修改)
|
||||
smtp_host=smtp.qq.com
|
||||
; SMTP服务器端口(SSL加密端口465,兼容性最强,无需修改,请勿改为25/587)
|
||||
smtp_port=465
|
||||
; 发送方邮箱(必填,需与SMTP授权码匹配,如QQ邮箱/163邮箱)
|
||||
send_email_account=[请填写发件人邮箱,如xxx@qq.com]
|
||||
; SMTP授权码(必填,非邮箱登录密码,需在邮箱后台开启SMTP服务后获取)
|
||||
; 获取方式:QQ邮箱-设置-账户-POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务-开启SMTP-生成授权码
|
||||
smtp_auth_code=[请填写邮箱SMTP授权码,非登录密码]
|
||||
; 发件人昵称(自定义,收件人看到的发件人名称,可随意修改)
|
||||
from_nickname=WinBoLLStudio
|
||||
; SMTP服务器连接超时时间(单位:毫秒,建议3000-10000,避免网络波动导致连接失败)
|
||||
smtp_connect_timeout=5000
|
||||
; 邮件主题前缀(验证码邮件的主题前缀,可自定义,如WinBoLL-认证验证码)
|
||||
email_subject_prefix=WinBoLLStudio
|
||||
; 验证码邮件内容模板({code}为占位符,会自动替换为实际6位验证码,请勿删除占位符)
|
||||
verify_code_email_content=您的认证验证码为:{code},有效期5分钟,请及时使用,请勿泄露给他人!
|
||||
|
||||
[VerifyCodeConfig]
|
||||
verify_code_length=【可选|正整数】验证码长度,默认填6
|
||||
verify_code_expire_minutes=【可选|正整数】验证码有效期(分钟),默认填5
|
||||
expire_code_clean_interval=【可选|正整数】过期验证码清理间隔(分钟),默认填1
|
||||
; 验证码长度(整型,建议6位,请勿修改为小于4位/大于8位)
|
||||
verify_code_length=6
|
||||
; 验证码有效期(单位:分钟,建议5-10分钟,平衡安全性和易用性)
|
||||
verify_code_expire_minutes=5
|
||||
; 过期验证码清理间隔(单位:分钟,建议1-5分钟,自动清理过期验证码,释放内存)
|
||||
expire_code_clean_interval=1
|
||||
|
||||
[HttpServiceConfig]
|
||||
http_server_port=【必填|端口号】HTTP服务监听端口,默认填8080(需和server_base_url端口一致)
|
||||
http_start_timeout=【可选|正整数】HTTP服务启动超时时间(毫秒),默认填3000
|
||||
; HTTP服务监听端口(整型,建议8080/8090,需保证端口未被占用,与server_base_url端口一致)
|
||||
http_server_port=8080
|
||||
; HTTP服务启动超时时间(单位:毫秒,建议3000-5000,等待服务完全启动的最大时间)
|
||||
http_start_timeout=3000
|
||||
|
||||
; 新增:测试用例专属配置节(可选,集中管理测试相关参数)
|
||||
; 新增:测试用例专属配置节(可选,仅自动化测试时生效,生产环境可忽略)
|
||||
[TestConfig]
|
||||
; 测试用例目录(基于PROJECT_ROOT_DIR拼接,无需绝对路径)
|
||||
test_case_dir=【可选】测试用例存放目录,默认填test/cases
|
||||
; 测试报告输出目录(测试结果保存路径)
|
||||
test_report_dir=【可选】测试报告输出目录,默认填test/reports
|
||||
; 测试数据文件路径(如测试用邮箱列表、测试验证码池)
|
||||
test_data_file=【可选】测试数据文件路径,默认填test/data/test_data.json
|
||||
|
||||
; 测试用例目录(基于root_path的相对路径,存储自动化测试用例脚本)
|
||||
test_case_dir=test/cases
|
||||
; 测试报告输出目录(基于root_path的相对路径,自动化测试完成后生成的报告存储路径)
|
||||
test_report_dir=test/reports
|
||||
; 测试数据文件路径(基于root_path的相对路径,存储测试用的基础数据,如测试邮箱列表、测试参数)
|
||||
test_data_file=test/data/test_data.json
|
||||
|
||||
330
src/cc/winboll/app/APKFileUtils.java
Normal file
330
src/cc/winboll/app/APKFileUtils.java
Normal file
@@ -0,0 +1,330 @@
|
||||
package cc.winboll.app;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import cc.winboll.util.IniConfigUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* APK文件工具类(单例)- 生产级签名+哈希双校验版(修复Too short异常,兼容MT重签名)
|
||||
* 1. 稳定解析META-INF下所有RSA原始字节,与客户端Signature.toByteArray()1:1对齐,解决X509解析异常
|
||||
* 2. 支持SHA256文件哈希字节级唯一校验,签名+哈希双重验证
|
||||
* 3. 入参包含:项目名/版本名/APK名/客户端签名/客户端哈希,适配生产级版本管理
|
||||
* 4. APK路径规范:apks_root/项目名/debug/tag/APK文件(支持调试/正式环境)
|
||||
* 5. 兼容MT重签名:遍历META-INF所有.RSA/.rsa文件,适配自定义签名文件命名
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class APKFileUtils {
|
||||
// 单例实例
|
||||
private static volatile APKFileUtils sInstance;
|
||||
// 配置项
|
||||
private static final String CONFIG_SECTION = "APP";
|
||||
private static final String KEY_APKS_FOLDER = "apks_folder_path";
|
||||
// 算法常量(与客户端严格对齐)
|
||||
private static final String SIGN_ALGORITHM = "SHA1"; // 签名摘要算法
|
||||
private static final String HASH_ALGORITHM = "SHA-256"; // 文件哈希算法
|
||||
// 签名文件目录与后缀(兼容MT重签名,遍历所有RSA文件)
|
||||
private static final String META_INF_DIR = "META-INF/";
|
||||
private static final String RSA_SUFFIX_UPPER = ".RSA";
|
||||
private static final String RSA_SUFFIX_LOWER = ".rsa";
|
||||
// APK根目录
|
||||
private String apksRootPath;
|
||||
|
||||
private APKFileUtils() {}
|
||||
|
||||
/**
|
||||
* 初始化工具类(需在应用启动时调用)
|
||||
*/
|
||||
public static void init() {
|
||||
if (sInstance == null) {
|
||||
synchronized (APKFileUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new APKFileUtils();
|
||||
sInstance.loadConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static APKFileUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
LogUtils.e("APKFileUtils", "请先调用init()初始化工具类");
|
||||
throw new IllegalStateException("APKFileUtils未初始化,请先调用init()");
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置文件中的APK根目录
|
||||
*/
|
||||
private void loadConfig() {
|
||||
try {
|
||||
apksRootPath = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_APKS_FOLDER, "").trim();
|
||||
if (apksRootPath.isEmpty()) {
|
||||
LogUtils.e("APKFileUtils", "配置项apks_folder_path为空,初始化失败");
|
||||
return;
|
||||
}
|
||||
File rootDir = new File(apksRootPath);
|
||||
if (!rootDir.exists() && !rootDir.mkdirs()) {
|
||||
LogUtils.e("APKFileUtils", "APK根目录创建失败:" + apksRootPath);
|
||||
apksRootPath = "";
|
||||
return;
|
||||
}
|
||||
LogUtils.i("APKFileUtils", "APK根目录加载成功:" + apksRootPath);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e("APKFileUtils", "加载APK根目录配置失败", e);
|
||||
apksRootPath = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外暴露核心校验方法:签名 + SHA256文件哈希 双校验
|
||||
* 入参包含:项目名/版本名/APK文件名/客户端签名Base64/客户端文件哈希
|
||||
* APK路径规范:apksRootPath/项目名/版本名/APK文件
|
||||
* @param isDebug 调试环境标识
|
||||
* @param projectName 项目名(非空)
|
||||
* @param versionName 版本名(非空,如15.11.11)
|
||||
* @param apkFileName APK文件名(非空,需以.apk结尾)
|
||||
* @param clientSignBase64 客户端传入的签名Base64(非空)
|
||||
* @param clientFileHash 客户端传入的APK文件SHA256哈希(小写/大写均可,非空)
|
||||
* @return 校验通过返回true,否则false
|
||||
*/
|
||||
public static boolean checkAPK(boolean isDebug, String projectName, String versionName, String apkFileName,
|
||||
String clientSignBase64, String clientFileHash) {
|
||||
return getInstance().doCheckAPK(isDebug, projectName, versionName, apkFileName, clientSignBase64, clientFileHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心校验实现:严格按「哈希先验,签名后验」顺序,哈希不匹配直接返回
|
||||
*/
|
||||
private boolean doCheckAPK(boolean isDebug, String projectName, String versionName, String apkFileName,
|
||||
String clientSignBase64, String clientFileHash) {
|
||||
// 1. 基础入参非空校验
|
||||
if (isParamEmpty(projectName) || isParamEmpty(versionName) || isParamEmpty(apkFileName)
|
||||
|| isParamEmpty(clientSignBase64) || isParamEmpty(clientFileHash)) {
|
||||
LogUtils.w("APKFileUtils", "基础参数不能为空:projectName/versionName/apkFileName/clientSignBase64/clientFileHash");
|
||||
return false;
|
||||
}
|
||||
// 2. APK文件名格式校验
|
||||
if (!apkFileName.endsWith(".apk")) {
|
||||
LogUtils.w("APKFileUtils", "APK文件名格式错误,需以.apk结尾:" + apkFileName);
|
||||
return false;
|
||||
}
|
||||
// 3. APK根目录校验
|
||||
if (isParamEmpty(apksRootPath)) {
|
||||
LogUtils.w("APKFileUtils", "APK根目录未配置,无法进行校验");
|
||||
return false;
|
||||
}
|
||||
// 4. 拼接标准APK路径:根目录/项目名/debug/项目名_版本名.apk(调试环境,可切换tag)
|
||||
String apkFullPath;
|
||||
if (isDebug) {
|
||||
apkFullPath = String.format("%s/%s/debug/%s_%s.apk",
|
||||
apksRootPath,
|
||||
projectName,
|
||||
projectName,
|
||||
versionName);
|
||||
} else {
|
||||
//正式环境路径(注释保留,切换时解开即可)
|
||||
apkFullPath = String.format("%s/%s/tag/%s_%s.apk",
|
||||
apksRootPath,
|
||||
projectName,
|
||||
projectName,
|
||||
versionName);
|
||||
}
|
||||
LogUtils.d("APKFileUtils", String.format("apkFullPath : %s", apkFullPath));
|
||||
File apkFile = new File(apkFullPath);
|
||||
// 5. APK文件存在性校验
|
||||
if (!apkFile.exists() || !apkFile.isFile()) {
|
||||
LogUtils.w("APKFileUtils", "APK文件不存在或非文件类型:" + apkFullPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// ===== 第一步:SHA256文件哈希校验(字节级唯一,优先级最高)=====
|
||||
String serverFileHash = getAPKFileHash(apkFile);
|
||||
if (isParamEmpty(serverFileHash)) {
|
||||
LogUtils.w("APKFileUtils", "解析服务端APK文件哈希失败:" + apkFileName);
|
||||
return false;
|
||||
}
|
||||
boolean isHashMatch = serverFileHash.equalsIgnoreCase(clientFileHash.trim());
|
||||
LogUtils.d("APKFileUtils", "【哈希对比】服务端SHA256:" + serverFileHash);
|
||||
LogUtils.d("APKFileUtils", "【哈希对比】客户端SHA256:" + clientFileHash.trim());
|
||||
if (!isHashMatch) {
|
||||
LogUtils.i("APKFileUtils", "【哈希对比结果】❌ 不匹配(字节级文件不一致)");
|
||||
return false;
|
||||
}
|
||||
LogUtils.i("APKFileUtils", "【哈希对比结果】✅ 匹配(字节级文件完全一致)");
|
||||
|
||||
// ===== 第二步:签名校验(遍历META-INF所有RSA文件,与客户端严格对齐)=====
|
||||
String serverSignBase64 = getAPKSign(apkFile);
|
||||
if (isParamEmpty(serverSignBase64)) {
|
||||
LogUtils.w("APKFileUtils", "解析服务端APK签名失败:" + apkFileName);
|
||||
return false;
|
||||
}
|
||||
boolean isSignMatch = serverSignBase64.equals(clientSignBase64.trim());
|
||||
LogUtils.d("APKFileUtils", "【签名对比】服务端Base64:" + serverSignBase64);
|
||||
LogUtils.d("APKFileUtils", "【签名对比】客户端Base64:" + clientSignBase64.trim());
|
||||
if (!isSignMatch) {
|
||||
LogUtils.i("APKFileUtils", "【签名对比结果】❌ 不匹配(签名不一致)");
|
||||
return false;
|
||||
}
|
||||
LogUtils.i("APKFileUtils", "【签名对比结果】✅ 匹配(签名完全一致)");
|
||||
|
||||
// 所有校验通过
|
||||
LogUtils.i("APKFileUtils", "APK双校验全部通过:项目名=" + projectName + ",版本名=" + versionName + ",文件名=" + apkFileName);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e("APKFileUtils", "APK双校验异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 稳定解析APK签名:遍历META-INF所有.RSA/.rsa文件,读取第一个有效文件原始字节,SHA1+Base64(与客户端1:1对齐)
|
||||
* 兼容MT重签名(自定义签名文件命名),解决X509证书解析的Too short异常,适配所有APK(普通/加固/重签名)
|
||||
* @param apkFile APK文件
|
||||
* @return 签名Base64字符串,失败返回null
|
||||
*/
|
||||
private String getAPKSign(File apkFile) {
|
||||
JarFile jarFile = null;
|
||||
InputStream certIs = null;
|
||||
try {
|
||||
jarFile = new JarFile(apkFile);
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
JarEntry targetRsaEntry = null;
|
||||
|
||||
// 核心改造:遍历META-INF下所有条目,找到第一个有效.RSA/.rsa文件(兼容MT重签名)
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
String entryName = entry.getName();
|
||||
// 过滤条件:META-INF目录下 + 非目录 + 以.RSA/.rsa结尾
|
||||
if (entryName.startsWith(META_INF_DIR) && !entry.isDirectory()
|
||||
&& (entryName.endsWith(RSA_SUFFIX_UPPER) || entryName.endsWith(RSA_SUFFIX_LOWER))) {
|
||||
targetRsaEntry = entry;
|
||||
LogUtils.d("APKFileUtils", "找到有效签名文件,路径:" + entryName);
|
||||
break; // 取第一个有效RSA文件即可,重签名APK仅会有一个签名文件
|
||||
}
|
||||
}
|
||||
|
||||
// 未找到任何RSA签名文件
|
||||
if (targetRsaEntry == null) {
|
||||
LogUtils.w("APKFileUtils", "APK中META-INF目录下未找到任何.RSA/.rsa签名文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取签名文件原始字节流(不做证书解析,适配PKCS7签名块,与客户端保持一致)
|
||||
certIs = jarFile.getInputStream(targetRsaEntry);
|
||||
byte[] sigRawBytes = readStreamToBytes(certIs);
|
||||
if (sigRawBytes == null || sigRawBytes.length == 0) {
|
||||
LogUtils.w("APKFileUtils", "读取签名文件原始字节为空:" + targetRsaEntry.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 与客户端完全一致的处理流程:SHA1摘要 → Base64编码(去换行,保证格式一致)
|
||||
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM);
|
||||
byte[] signDigest = md.digest(sigRawBytes);
|
||||
String signBase64 = Base64.getEncoder().encodeToString(signDigest)
|
||||
.replaceAll("\\r", "").replaceAll("\\n", "");
|
||||
|
||||
LogUtils.d("APKFileUtils", "APK签名解析成功(Base64):" + signBase64);
|
||||
return signBase64;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
LogUtils.e("APKFileUtils", "解析签名失败:" + SIGN_ALGORITHM + "算法不存在", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e("APKFileUtils", "解析APK签名异常", e);
|
||||
return null;
|
||||
} finally {
|
||||
// 强制关闭流资源,避免内存泄漏
|
||||
try {
|
||||
if (certIs != null) certIs.close();
|
||||
if (jarFile != null) jarFile.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e("APKFileUtils", "关闭签名文件流失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析APK文件的SHA256哈希(字节级唯一,任何字节修改都会改变)
|
||||
* @param apkFile APK文件
|
||||
* @return 小写64位SHA256哈希字符串,失败返回null
|
||||
*/
|
||||
private String getAPKFileHash(File apkFile) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
|
||||
fis = new FileInputStream(apkFile);
|
||||
byte[] buffer = new byte[8192]; // 8K缓冲区,提升大APK读取效率
|
||||
int len;
|
||||
while ((len = fis.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, len);
|
||||
}
|
||||
// 哈希字节转小写16进制字符串(64位,官方标准格式)
|
||||
byte[] hashBytes = md.digest();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
String fileHash = sb.toString();
|
||||
LogUtils.d("APKFileUtils", "APK文件SHA256哈希解析成功:" + fileHash);
|
||||
return fileHash;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
LogUtils.e("APKFileUtils", "获取文件哈希失败:" + HASH_ALGORITHM + "算法不存在", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e("APKFileUtils", "解析APK文件哈希异常", e);
|
||||
return null;
|
||||
} finally {
|
||||
if (fis != null) {
|
||||
try {
|
||||
fis.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e("APKFileUtils", "关闭APK文件流失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 流转字节数组工具方法:稳定读取任意输入流,无截断/空指针问题
|
||||
*/
|
||||
private byte[] readStreamToBytes(InputStream is) throws IOException {
|
||||
if (is == null) {
|
||||
LogUtils.w("APKFileUtils", "readStreamToBytes: 输入流为null");
|
||||
return new byte[0];
|
||||
}
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, len);
|
||||
}
|
||||
byte[] result = bos.toByteArray();
|
||||
// 按顺序关闭流
|
||||
is.close();
|
||||
bos.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:判断参数是否为空(null/空字符串/全空格)
|
||||
*/
|
||||
private boolean isParamEmpty(String param) {
|
||||
return param == null || param.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
package cc.winboll.service;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import cc.winboll.util.ServerUtils;
|
||||
import cc.winboll.util.ConsoleVersionUtils;
|
||||
import cc.winboll.app.APKFileUtils;
|
||||
import cc.winboll.util.IniConfigUtils;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 独立HTTP监听服务类,仅负责请求接收与分发,业务逻辑完全依赖ServerUtils
|
||||
* HTTP监听服务类,仅负责服务启停、请求接收与分发
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026-01-15 23:45:00
|
||||
* @LastEditTime 新增/api/version版本查询接口
|
||||
* @LastEditTime 2026-01-22 修复GET参数解析+签名接口路由
|
||||
*/
|
||||
public class AuthCenterHttpService extends NanoHTTPD {
|
||||
private static final String TAG = "AuthCenterHttpService";
|
||||
private boolean isRunning = false;
|
||||
// 依赖业务处理工具类(通过构造注入,便于测试)
|
||||
private final AuthHttpHandler mHttpHandler;
|
||||
|
||||
// 构造方法:传入业务处理工具类
|
||||
public AuthCenterHttpService(int port) {
|
||||
super(port);
|
||||
LogUtils.d(TAG, "构造方法调用,监听端口:" + port);
|
||||
this.mHttpHandler = new AuthHttpHandler(); // 默认初始化工具类
|
||||
LogUtils.d(TAG, "构造方法调用,监听端口:" + port + ",业务处理工具类初始化完成");
|
||||
}
|
||||
|
||||
// 重载构造:支持外部注入工具类(便于单元测试)
|
||||
public AuthCenterHttpService(int port, AuthHttpHandler httpHandler) {
|
||||
super(port);
|
||||
this.mHttpHandler = httpHandler;
|
||||
LogUtils.d(TAG, "构造方法调用(外部注入工具类),监听端口:" + port);
|
||||
}
|
||||
|
||||
public void start() throws IOException {
|
||||
LogUtils.d(TAG, "start() 函数调用,启动HTTP监听服务");
|
||||
// ========== 必须按此顺序添加(核心修复)==========
|
||||
IniConfigUtils.init(); // 1. 先初始化配置工具
|
||||
LogUtils.init(); // 2. 再初始化日志工具
|
||||
APKFileUtils.init(); // 3. 最后初始化APK签名校验工具
|
||||
// ==============================================
|
||||
super.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
isRunning = true;
|
||||
LogUtils.i(TAG, "HTTP监听服务启动成功,端口:" + getListeningPort());
|
||||
@@ -48,111 +63,78 @@ public class AuthCenterHttpService extends NanoHTTPD {
|
||||
LogUtils.d(TAG, "接收请求:method=" + method.name() + ",原始uri=" + rawUri + ",规范化uri=" + normUri);
|
||||
|
||||
try {
|
||||
// 新增 /api/version 版本查询接口(GET)
|
||||
if (Method.GET.equals(method) && "/api/version".equals(normUri)) {
|
||||
return handleVersionQuery();
|
||||
} else if (Method.GET.equals(method) && "/".equals(normUri)) {
|
||||
return handleHelloWorld();
|
||||
// 请求路由分发(仅做判断,业务逻辑交给工具类)
|
||||
if (Method.GET.equals(method)) {
|
||||
return handleGetRequest(normUri, session); // 传session
|
||||
} else if (Method.POST.equals(method)) {
|
||||
return handlePostRequest(normUri, session);
|
||||
} else {
|
||||
LogUtils.d(TAG, "非目标请求,返回404");
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "404 Not Found");
|
||||
LogUtils.w(TAG, "不支持的请求方法:" + method.name());
|
||||
return mHttpHandler.buildErrorResponse(Response.Status.METHOD_NOT_ALLOWED, "不支持的请求方法");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "请求处理异常", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "application/json",
|
||||
"{\"code\":500,\"msg\":\"服务内部异常\",\"data\":null}");
|
||||
return mHttpHandler.buildErrorResponse(Response.Status.INTERNAL_ERROR, "服务内部异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理根路径默认请求
|
||||
*/
|
||||
private Response handleHelloWorld() {
|
||||
LogUtils.d(TAG, "根路径请求响应成功,返回服务运行标识");
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/plain", "WinBoLL is Running...");
|
||||
}
|
||||
// 修复:GET路由传session,用NanoHTTPD原生方法解析参数(更可靠)
|
||||
private Response handleGetRequest(String normUri, IHTTPSession session) {
|
||||
// 优先处理favicon.ico,消除冗余日志
|
||||
if ("/favicon.ico".equals(normUri)) {
|
||||
return NanoHTTPD.newFixedLengthResponse(Response.Status.NO_CONTENT, "text/plain", "");
|
||||
}
|
||||
|
||||
// 新增:版本查询接口实现,调用ConsoleVersionUtils返回结果
|
||||
private Response handleVersionQuery() {
|
||||
String version = ConsoleVersionUtils.getVersion();
|
||||
LogUtils.d(TAG, "版本查询请求响应成功,当前版本结果:" + version);
|
||||
// 响应text/plain,直接返回字符串结果,贴合需求
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/plain", version);
|
||||
}
|
||||
|
||||
// 处理ping请求
|
||||
private Response handlePingRequest() {
|
||||
LogUtils.d(TAG, "ping请求响应成功,返回pong");
|
||||
return newFixedLengthResponse(Response.Status.OK, "application/json",
|
||||
"{\"code\":200,\"msg\":\"pong\",\"data\":null}");
|
||||
}
|
||||
|
||||
// 发送验证码:修改为调用本地方法,解决递归问题
|
||||
private Response handleSendVerifyCode(IHTTPSession session) throws IOException, NanoHTTPD.ResponseException{
|
||||
session.parseBody(null);
|
||||
// 原生解析GET参数,无需自己拆分URI
|
||||
Map<String, String> params = session.getParms();
|
||||
String email = params.get("email");
|
||||
LogUtils.d(TAG, "接收验证码发送请求,邮箱:" + email);
|
||||
|
||||
// 核心修改:替换为本地发送方法,返回布尔值
|
||||
boolean sendResult = ServerUtils.sendVerifyCodeLocal(email);
|
||||
// 统一返回JSON格式,与其他接口保持一致
|
||||
String responseJson;
|
||||
if (sendResult) {
|
||||
responseJson = "{\"code\":200,\"msg\":\"验证码发送成功\",\"data\":null}";
|
||||
} else {
|
||||
responseJson = "{\"code\":500,\"msg\":\"验证码发送失败\",\"data\":null}";
|
||||
switch (normUri) {
|
||||
case "/":
|
||||
return mHttpHandler.handleHelloWorld();
|
||||
case "/api/version":
|
||||
return mHttpHandler.handleVersionQuery();
|
||||
case "/ping":
|
||||
return mHttpHandler.handlePingRequest();
|
||||
// 签名校验接口:传原生解析的参数
|
||||
case "/api/app-signatures-check":
|
||||
return mHttpHandler.handleAppSignatureCheck(params);
|
||||
default:
|
||||
LogUtils.d(TAG, "GET请求未匹配:" + normUri);
|
||||
return mHttpHandler.buildErrorResponse(Response.Status.NOT_FOUND, "404 Not Found");
|
||||
}
|
||||
return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson);
|
||||
}
|
||||
|
||||
// 校验验证码:直接调用ServerUtils
|
||||
private Response handleVerifyCode(IHTTPSession session) throws IOException, NanoHTTPD.ResponseException {
|
||||
session.parseBody(null);
|
||||
Map<String, String> params = session.getParms();
|
||||
String email = params.get("email");
|
||||
String code = params.get("code");
|
||||
LogUtils.d(TAG, "接收验证码校验请求,邮箱:" + email + ",验证码:" + code);
|
||||
// 移除自己写的getParameters方法(原生session.getParms()更稳定,避免拆分bug)
|
||||
|
||||
String result = ServerUtils.verifyCode(email, code);
|
||||
// 兜底处理null值,避免返回空响应
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"校验失败\",\"data\":null}";
|
||||
// 处理POST请求路由
|
||||
private Response handlePostRequest(String normUri, IHTTPSession session) throws IOException, ResponseException {
|
||||
// 解析请求参数(后续要和AuthHttpHandler的parseRequestParams统一)
|
||||
Map<String, String> params = mHttpHandler.parseRequestParams(session);
|
||||
if (params == null) {
|
||||
return mHttpHandler.buildErrorResponse(Response.Status.BAD_REQUEST, "参数解析失败");
|
||||
}
|
||||
return newFixedLengthResponse(Response.Status.OK, "application/json", result);
|
||||
}
|
||||
|
||||
// 提交公钥:直接调用ServerUtils
|
||||
private Response handleSubmitPublicKey(IHTTPSession session) throws IOException, NanoHTTPD.ResponseException {
|
||||
session.parseBody(null);
|
||||
Map<String, String> params = session.getParms();
|
||||
String email = params.get("email");
|
||||
String publicKey = params.get("appPublicKey");
|
||||
LogUtils.d(TAG, "接收公钥提交请求,邮箱:" + email + ",公钥:" + publicKey);
|
||||
|
||||
String result = ServerUtils.submitAppPublicKey(email, publicKey);
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"公钥提交失败\",\"data\":null}";
|
||||
switch (normUri) {
|
||||
case "/send-verify-code":
|
||||
return mHttpHandler.handleSendVerifyCode(params);
|
||||
case "/verify-code":
|
||||
return mHttpHandler.handleVerifyCode(params);
|
||||
case "/submit-public-key":
|
||||
return mHttpHandler.handleSubmitPublicKey(params);
|
||||
case "/heartbeat-ping":
|
||||
return mHttpHandler.handleHeartbeatPing(params);
|
||||
default:
|
||||
LogUtils.d(TAG, "POST请求未匹配:" + normUri);
|
||||
return mHttpHandler.buildErrorResponse(Response.Status.NOT_FOUND, "404 Not Found");
|
||||
}
|
||||
return newFixedLengthResponse(Response.Status.OK, "application/json", result);
|
||||
}
|
||||
|
||||
// 心跳请求:直接调用ServerUtils
|
||||
private Response handleHeartbeatPing(IHTTPSession session) throws IOException, NanoHTTPD.ResponseException {
|
||||
session.parseBody(null);
|
||||
Map<String, String> params = session.getParms();
|
||||
String encryptData = params.get("encryptData");
|
||||
LogUtils.d(TAG, "接收心跳请求,加密数据:" + encryptData);
|
||||
|
||||
String result = ServerUtils.sendPingRequest(encryptData);
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"心跳请求失败\",\"data\":null}";
|
||||
}
|
||||
return newFixedLengthResponse(Response.Status.OK, "application/json", result);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
// 对外暴露工具类(可选,便于外部扩展)
|
||||
public AuthHttpHandler getHttpHandler() {
|
||||
return mHttpHandler;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
238
src/cc/winboll/service/AuthHttpHandler.java
Normal file
238
src/cc/winboll/service/AuthHttpHandler.java
Normal file
@@ -0,0 +1,238 @@
|
||||
package cc.winboll.service;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import cc.winboll.app.APKFileUtils;
|
||||
import cc.winboll.util.ServerUtils;
|
||||
import cc.winboll.util.ConsoleVersionUtils;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* HTTP接口业务处理工具类,封装所有接口逻辑,无HTTP依赖
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026-01-21 16:30:00
|
||||
*/
|
||||
public class AuthHttpHandler {
|
||||
private static final String TAG = "AuthHttpHandler";
|
||||
private static final String CONTENT_TYPE_JSON = "application/json";
|
||||
private static final String CONTENT_TYPE_PLAIN = "text/plain";
|
||||
|
||||
/**
|
||||
* 构建JSON错误响应
|
||||
*/
|
||||
public NanoHTTPD.Response buildErrorResponse(NanoHTTPD.Response.Status status, String msg) {
|
||||
String json = String.format("{\"code\":%d,\"msg\":\"%s\",\"data\":null}", status.getRequestStatus(), msg);
|
||||
return NanoHTTPD.newFixedLengthResponse(status, CONTENT_TYPE_JSON, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用参数解析(兼容GET/POST),替换原parsePostParams
|
||||
*/
|
||||
public Map<String, String> parseRequestParams(NanoHTTPD.IHTTPSession session) throws IOException, NanoHTTPD.ResponseException {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
// 先取URL参数(优先级高)
|
||||
params.putAll(session.getParms());
|
||||
// 再取POST Body参数,重复key覆盖URL参数
|
||||
session.parseBody(params);
|
||||
LogUtils.d(TAG, "解析请求参数:" + params);
|
||||
return params.isEmpty() ? null : params;
|
||||
}
|
||||
|
||||
// ==================== 接口业务逻辑实现 ====================
|
||||
/**
|
||||
* 根路径默认响应
|
||||
*/
|
||||
public NanoHTTPD.Response handleHelloWorld() {
|
||||
LogUtils.d(TAG, "处理根路径请求");
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_PLAIN, "WinBoLL is Running...");
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本查询接口
|
||||
*/
|
||||
public NanoHTTPD.Response handleVersionQuery() {
|
||||
LogUtils.d(TAG, "处理版本查询请求");
|
||||
String version = ConsoleVersionUtils.getVersion();
|
||||
LogUtils.d(TAG, "版本查询结果:" + version);
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_PLAIN, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping请求响应
|
||||
*/
|
||||
public NanoHTTPD.Response handlePingRequest() {
|
||||
LogUtils.d(TAG, "处理ping请求");
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON,
|
||||
"{\"code\":200,\"msg\":\"pong\",\"data\":null}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码接口
|
||||
*/
|
||||
public NanoHTTPD.Response handleSendVerifyCode(Map<String, String> params) {
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "发送验证码失败:未获取到参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
String email = params.get("email");
|
||||
LogUtils.d(TAG, "处理验证码发送请求,邮箱:" + email);
|
||||
|
||||
if (email == null || email.isEmpty()) {
|
||||
LogUtils.w(TAG, "发送验证码失败:邮箱为空");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "邮箱不能为空");
|
||||
}
|
||||
|
||||
boolean sendResult = ServerUtils.sendVerifyCodeLocal(email);
|
||||
String json = sendResult ?
|
||||
"{\"code\":200,\"msg\":\"验证码发送成功\",\"data\":null}" :
|
||||
"{\"code\":500,\"msg\":\"验证码发送失败\",\"data\":null}";
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码接口
|
||||
*/
|
||||
public NanoHTTPD.Response handleVerifyCode(Map<String, String> params) {
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "校验验证码失败:未获取到参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
String email = params.get("email");
|
||||
String code = params.get("code");
|
||||
LogUtils.d(TAG, "处理验证码校验请求,邮箱:" + email + ",验证码:" + code);
|
||||
|
||||
if (email == null || code == null || email.isEmpty() || code.isEmpty()) {
|
||||
LogUtils.w(TAG, "校验验证码失败:参数不全");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "邮箱和验证码不能为空");
|
||||
}
|
||||
|
||||
String result = ServerUtils.verifyCode(email, code);
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"校验失败\",\"data\":null}";
|
||||
}
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交公钥接口
|
||||
*/
|
||||
public NanoHTTPD.Response handleSubmitPublicKey(Map<String, String> params) {
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "提交公钥失败:未获取到参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
String email = params.get("email");
|
||||
String publicKey = params.get("appPublicKey");
|
||||
LogUtils.d(TAG, "处理公钥提交请求,邮箱:" + email + ",公钥:" + publicKey);
|
||||
|
||||
if (email == null || publicKey == null || email.isEmpty() || publicKey.isEmpty()) {
|
||||
LogUtils.w(TAG, "提交公钥失败:参数不全");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "邮箱和公钥不能为空");
|
||||
}
|
||||
|
||||
String result = ServerUtils.submitAppPublicKey(email, publicKey);
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"公钥提交失败\",\"data\":null}";
|
||||
}
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳请求接口
|
||||
*/
|
||||
public NanoHTTPD.Response handleHeartbeatPing(Map<String, String> params) {
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "心跳请求失败:未获取到参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
String encryptData = params.get("encryptData");
|
||||
LogUtils.d(TAG, "处理心跳请求,加密数据:" + encryptData);
|
||||
|
||||
if (encryptData == null || encryptData.isEmpty()) {
|
||||
LogUtils.w(TAG, "心跳请求失败:加密数据为空");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "加密数据不能为空");
|
||||
}
|
||||
|
||||
String result = ServerUtils.sendPingRequest(encryptData);
|
||||
if (result == null) {
|
||||
result = "{\"code\":500,\"msg\":\"心跳请求失败\",\"data\":null}";
|
||||
}
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用签名+文件哈希双重校验接口(处理/api/app-signatures-check请求)
|
||||
* 适配URI:http://localhost:8080/api/app-signatures-check?projectName=WinBoLL&versionName=15.11.11&clientSign=xxx&clientHash=xxx
|
||||
* 核心:官方签名校验 + SHA256文件哈希校验,全异常捕获,避免500错误
|
||||
*/
|
||||
public NanoHTTPD.Response handleAppSignatureCheck(Map<String, String> params) {
|
||||
LogUtils.d(TAG, "处理应用签名+哈希双重校验请求,参数:" + params);
|
||||
// 全局异常捕获:解决500内部错误
|
||||
try {
|
||||
// 前置判空
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "双重校验失败:未获取到任何参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
|
||||
// 1. 解析新核心必选参数(projectName/versionName/clientSign/clientHash)
|
||||
String szIsDebug = params.get("isDebug");
|
||||
String projectName = params.get("projectName");
|
||||
String versionName = params.get("versionName");
|
||||
String clientSign = params.get("clientSign");
|
||||
String clientHash = params.get("clientHash");
|
||||
|
||||
// 2. 必选参数非空+格式基础校验
|
||||
if (szIsDebug == null || szIsDebug.trim().isEmpty()
|
||||
|| projectName == null || projectName.trim().isEmpty()
|
||||
|| versionName == null || versionName.trim().isEmpty()
|
||||
|| clientSign == null || clientSign.trim().isEmpty()
|
||||
|| clientHash == null || clientHash.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "双重校验失败:必选参数不全(projectName/versionName/clientSign/clientHash不能为空)");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
"参数错误:projectName、versionName、clientSign、clientHash为必填参数");
|
||||
}
|
||||
// 校验哈希长度(SHA256固定64位16进制,过滤明显非法值)
|
||||
if (clientHash.trim().length() != 64) {
|
||||
LogUtils.w(TAG, "双重校验失败:clientHash格式错误,需为64位SHA256哈希");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
"参数错误:clientHash需为64位SHA256十六进制哈希字符串");
|
||||
}
|
||||
|
||||
// 3. 核心双校验:调用改造后的APKFileUtils.checkAPK(官方签名+文件哈希)
|
||||
// 注:APK文件名拼接规则:项目名_版本名.apk(与客户端包名/版本名对齐,规范命名)
|
||||
String apkFileName = String.format("%s_%s.apk", projectName.trim(), versionName.trim());
|
||||
boolean isValid = APKFileUtils.checkAPK(szIsDebug.trim().equals("true"), projectName.trim(), versionName.trim(),
|
||||
apkFileName, clientSign.trim(), clientHash.trim());
|
||||
|
||||
// 4. 日志输出详情(便于问题排查,含完整校验维度)
|
||||
LogUtils.d(TAG, String.format("签名+哈希双重校验结果:%s | 项目名:%s | 版本名:%s | APK文件名:%s | 客户端签名:%s | 客户端哈希:%s",
|
||||
isValid ? "✅ 成功" : "❌ 失败",
|
||||
projectName.trim(), versionName.trim(), apkFileName,
|
||||
clientSign.trim(), clientHash.trim()));
|
||||
|
||||
// 5. 构建响应结果(与APP端预期格式一致,字段简洁无冗余,含核心返参)
|
||||
String msg = isValid ? "应用签名+文件哈希双重校验通过,为合法正版应用" : "应用校验失败:签名或文件哈希不匹配,非正版/篡改应用";
|
||||
int code = isValid ? 200 : 403;
|
||||
|
||||
String responseJson = String.format(
|
||||
"{\"code\":%d,\"msg\":\"%s\",\"data\":{\"valid\":%b,\"projectName\":\"%s\",\"versionName\":\"%s\",\"apkFileName\":\"%s\"}}",
|
||||
code, msg, isValid, projectName.trim(), versionName.trim(), apkFileName
|
||||
);
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, responseJson);
|
||||
} catch (IllegalStateException e) {
|
||||
// 捕获APKFileUtils未初始化异常(最常见原因)
|
||||
LogUtils.e(TAG, "双重校验失败:APKFileUtils未初始化,请在服务启动时调用APKFileUtils.init()", e);
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常:APKFileUtils未初始化");
|
||||
} catch (Exception e) {
|
||||
// 捕获所有其他异常,打印完整堆栈(定位根因)
|
||||
LogUtils.e(TAG, "签名+哈希双重校验失败:服务内部异常", e);
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@ import cc.winboll.auth.MailAuthUtils;
|
||||
import cc.winboll.auth.RSAUtils;
|
||||
import cc.winboll.models.UserModel;
|
||||
import cc.winboll.util.ConsoleInputUtils;
|
||||
import cc.winboll.util.ConsoleVersionUtils;
|
||||
import cc.winboll.util.EmailSendUtils;
|
||||
import cc.winboll.util.EnvInfoUtils;
|
||||
import cc.winboll.util.HelpInfoUtils;
|
||||
import cc.winboll.util.IniConfigUtils;
|
||||
import cc.winboll.util.MainUtils;
|
||||
import cc.winboll.util.ServerUtils;
|
||||
import cc.winboll.util.IniConfigUtils;
|
||||
import cc.winboll.util.EnvInfoUtils;
|
||||
import cc.winboll.util.ConsoleVersionUtils; // 1. 新增导入
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
@@ -25,11 +26,11 @@ import java.util.List;
|
||||
* AuthCenter 控制台指令自动化测试类
|
||||
* 适配 Java7 语言规范与 Android API30 运行环境
|
||||
* 自动执行控制台指令测试用例,生成结构化测试报告 + 发送报告到配置邮箱
|
||||
* 核心优化:从config.ini读取配置,动态初始化测试路径与参数,根目录配置缺失直接日志退出
|
||||
* 本次更新:1.新增EnvInfoUtils测试用例 2.邮件报告追加环境信息 3.用EmailSendUtils无参test()+修复用例索引错误+精简冗余日志
|
||||
* 核心优化:IniConfigUtils+LogUtils移至main直接初始化,移除其单元测试,流程更简洁
|
||||
* 本次更新:移除核心配置工具类单元测试,main函数前置初始化核心依赖
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/20 10:00:00
|
||||
* @LastEditTime 2026/01/26 新增EnvInfoUtils用例+邮件追加环境信息+补充用户ID展示 + 新增ConsoleVersionUtils版本读取测试
|
||||
* @LastEditTime 2026/01/26 main前置初始化IniConfigUtils+LogUtils,移除对应测试用例
|
||||
*/
|
||||
public class ConsoleCmdAutoTest {
|
||||
// ========== 静态常量(配置键名,不可变) ==========
|
||||
@@ -40,6 +41,7 @@ public class ConsoleCmdAutoTest {
|
||||
private static final String CONFIG_SECTION_GLOBAL = "GlobalConfig";
|
||||
private static final String CONFIG_SECTION_EMAIL = "EmailConfig";
|
||||
private static final String CONFIG_KEY_SEND_EMAIL = "send_email_account";
|
||||
private static final String CONFIG_KEY_SMTP_AUTH_CODE = "smtp_auth_code";// 授权码配置键
|
||||
|
||||
// ========== 动态配置属性(从INI读取,可变) ==========
|
||||
private static String PROJECT_ROOT_DIR;
|
||||
@@ -69,20 +71,29 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 程序入口主函数 ==========
|
||||
// ========== 程序入口主函数 ==========
|
||||
public static boolean main(String[] args) {
|
||||
// 核心改动:main函数前置初始化核心依赖,优先完成IniConfig+LogUtils初始化
|
||||
try {
|
||||
IniConfigUtils.init();// 优先初始化配置工具
|
||||
LogUtils.init();// 初始化日志工具
|
||||
System.out.println("✅ 核心依赖初始化完成(IniConfigUtils+LogUtils)");
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 核心依赖初始化失败,程序退出:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "【函数调用】main(),自动化测试程序启动");
|
||||
|
||||
// 初始化报告路径(读取GlobalConfig的reports_path,优先级最高)
|
||||
initReportPathFromIni();
|
||||
|
||||
// 直接执行测试流程(依赖外部已初始化配置)
|
||||
// 直接执行测试流程
|
||||
registerTestCases();
|
||||
runAllTestCases();
|
||||
generateTestReport();
|
||||
|
||||
// 新增:生成报告后发送到配置邮箱
|
||||
// 生成报告后发送到配置邮箱
|
||||
sendReportToEmail();
|
||||
|
||||
// 输出测试总结
|
||||
@@ -93,17 +104,16 @@ public class ConsoleCmdAutoTest {
|
||||
System.out.println("报告文件路径:" + new File(REPORT_FILE).getAbsolutePath());
|
||||
LogUtils.i(TAG, "【函数结束】main()," + summary);
|
||||
|
||||
// 核心修改:任意用例失败返回false,全部通过返回true
|
||||
// 任意用例失败返回false,全部通过返回true
|
||||
return failCount == 0;
|
||||
}
|
||||
|
||||
// ========== 核心修改:读取GlobalConfig的reports_path初始化报告路径 ==========
|
||||
// ========== 读取GlobalConfig的reports_path初始化报告路径 ==========
|
||||
private static void initReportPathFromIni() {
|
||||
try {
|
||||
// 读取核心配置
|
||||
PROJECT_ROOT_DIR = IniConfigUtils.getConfigValue(CONFIG_SECTION_GLOBAL, CONFIG_KEY_ROOT_DIR, "");
|
||||
LOG_DIR_PATH = IniConfigUtils.getConfigValue(CONFIG_SECTION_GLOBAL, CONFIG_KEY_LOG_PATH, "logs");
|
||||
// 核心修改:读取GlobalConfig下的reports_path作为报告根目录
|
||||
REPORT_DIR_PATH = IniConfigUtils.getConfigValue(CONFIG_SECTION_GLOBAL, CONFIG_KEY_REPORTS_PATH, "test/reports");
|
||||
|
||||
// 拼接完整报告文件路径,文件名带时间戳防重复
|
||||
@@ -122,7 +132,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新增:发送测试报告到配置邮箱(追加环境信息) ==========
|
||||
// ========== 发送测试报告到配置邮箱(追加环境信息) ==========
|
||||
private static void sendReportToEmail() {
|
||||
LogUtils.d(TAG, "【函数调用】sendReportToEmail(),开始发送测试报告邮件");
|
||||
EmailSendUtils emailUtils = EmailSendUtils.getInstance();
|
||||
@@ -139,7 +149,7 @@ public class ConsoleCmdAutoTest {
|
||||
return;
|
||||
}
|
||||
|
||||
// 核心更新:拼接EnvInfoUtils完整环境报告,格式和终端一致
|
||||
// 拼接EnvInfoUtils完整环境报告
|
||||
String envInfo = getEnvReportForEmail();
|
||||
String finalContent = reportContent + "\n\n" + envInfo;
|
||||
|
||||
@@ -165,20 +175,18 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 更新:组装邮件用的环境信息(格式统一+无冗余) ==========
|
||||
// ========== 组装邮件用的环境信息 ==========
|
||||
private static String getEnvReportForEmail() {
|
||||
StringBuilder envSb = new StringBuilder();
|
||||
envSb.append("=== 运行环境核心信息(EnvInfoUtils获取)===\n");
|
||||
envSb.append("核心环境标识:").append(EnvInfoUtils.getEnvFlag()).append("\n");
|
||||
envSb.append("是否Android环境:").append(EnvInfoUtils.isAndroidEnv() ? "是" : "否").append("\n\n");
|
||||
|
||||
// 核心:直接拼接完整环境报告,和终端输出完全一致
|
||||
envSb.append(EnvInfoUtils.getFullEnvReportText()).append("\n");
|
||||
envSb.append("=====================================");
|
||||
return envSb.toString();
|
||||
}
|
||||
|
||||
// ========== 新增:读取报告文件内容 ==========
|
||||
// ========== 读取报告文件内容 ==========
|
||||
private static String readReportFile() {
|
||||
BufferedReader reader = null;
|
||||
StringBuilder content = new StringBuilder();
|
||||
@@ -203,22 +211,21 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 工具函数(按执行顺序排列) ==========
|
||||
// ========== 注册测试用例(已移除IniConfigUtils、LogUtils相关测试) ==========
|
||||
private static void registerTestCases() {
|
||||
LogUtils.d(TAG, "【函数调用】registerTestCases(),开始注册测试用例");
|
||||
final EmailSendUtils emailUtils = EmailSendUtils.getInstance();
|
||||
|
||||
// ========== 3个核心工具类独立测试(优先执行) ==========
|
||||
// 用例01:核心工具类集成测试(仅保留RSAUtils/UserModel)
|
||||
testCaseList.add(new TestCase(
|
||||
"核心工具类集成测试",
|
||||
"执行LogUtils/RSAUtils/UserModel独立测试函数,验证工具类完整性",
|
||||
"执行RSAUtils/UserModel独立测试函数,验证工具类完整性",
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
LogUtils.test(); // 执行LogUtils测试
|
||||
RSAUtils.test(); // 执行RSAUtils测试
|
||||
UserModel.test(); // 执行UserModel测试
|
||||
RSAUtils.test(); // 仅保留RSA测试
|
||||
UserModel.test(); // 仅保留UserModel测试
|
||||
testCaseList.get(0).isPass = true;
|
||||
LogUtils.d(TAG, "【用例结果】核心工具类集成测试通过");
|
||||
} catch (Exception e) {
|
||||
@@ -229,7 +236,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 新增用例 ConsoleVersionUtils版本读取测试
|
||||
// 用例02:ConsoleVersionUtils版本读取测试
|
||||
testCaseList.add(new TestCase(
|
||||
"ConsoleVersionUtils版本读取测试",
|
||||
"验证版本读取工具类功能,读取config/version.flags并校验格式",
|
||||
@@ -237,11 +244,8 @@ public class ConsoleCmdAutoTest {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 执行版本工具自测
|
||||
ConsoleVersionUtils.test();
|
||||
// 验证核心读取功能
|
||||
String version = ConsoleVersionUtils.getVersion();
|
||||
// 只要不抛异常即算通过,兼容各种返回场景
|
||||
testCaseList.get(1).isPass = true;
|
||||
LogUtils.d(TAG, "【用例结果】版本读取测试通过,当前版本结果:" + version);
|
||||
} catch (Exception e) {
|
||||
@@ -252,7 +256,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 新增用例02:EnvInfoUtils环境检测工具测试(紧跟核心工具类)
|
||||
// 用例03:EnvInfoUtils环境检测工具测试
|
||||
testCaseList.add(new TestCase(
|
||||
"EnvInfoUtils环境检测测试",
|
||||
"验证环境信息工具类核心功能,输出环境标识+完整环境报告",
|
||||
@@ -260,9 +264,7 @@ public class ConsoleCmdAutoTest {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 执行工具类自测
|
||||
EnvInfoUtils.test();
|
||||
// 验证核心函数有效性
|
||||
String envFlag = EnvInfoUtils.getEnvFlag();
|
||||
String javaVer = EnvInfoUtils.getEnvInfo(EnvInfoUtils.PROP_JAVA_VERSION);
|
||||
if (!EmailSendUtils.isStrEmpty(envFlag) && !EmailSendUtils.isStrEmpty(javaVer)) {
|
||||
@@ -280,7 +282,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 用例03:help指令响应测试(索引同步后移)
|
||||
// 用例04:help指令响应测试
|
||||
testCaseList.add(new TestCase(
|
||||
"help指令响应测试",
|
||||
"验证help指令能否正常打印帮助信息",
|
||||
@@ -300,45 +302,59 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 用例04:selftestmail指令测试 - 用无参test()+精简日志+精准排错(索引同步后移)
|
||||
// 用例05:selftestmail指令测试(优化版)
|
||||
testCaseList.add(new TestCase(
|
||||
"selftestmail指令测试",
|
||||
"验证邮件自测功能(需配置INI邮箱账号、授权码,且依赖齐全)",
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TestCase currentCase = testCaseList.get(4);
|
||||
try {
|
||||
LogUtils.d(TAG, "邮件自测开始,使用无参test自动生成测试码");
|
||||
LogUtils.d(TAG, "邮件自测开始,前置校验配置+独立初始化");
|
||||
|
||||
// 1. 先检查配置是否就绪,不就绪先初始化
|
||||
if (!emailUtils.isConfigInited()) {
|
||||
LogUtils.d(TAG, "邮件配置未初始化,尝试自动初始化");
|
||||
boolean initOk = emailUtils.initEmailConfig();
|
||||
if (!initOk) {
|
||||
testCaseList.get(4).failReason = "邮件配置初始化失败,检查INI[EmailConfig] 下send_email_account和smtp_auth_code";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + testCaseList.get(4).failReason);
|
||||
return;
|
||||
}
|
||||
// 1. 前置核心配置校验
|
||||
String sendEmail = IniConfigUtils.getConfigValue(CONFIG_SECTION_EMAIL, CONFIG_KEY_SEND_EMAIL, "");
|
||||
String authCode = IniConfigUtils.getConfigValue(CONFIG_SECTION_EMAIL, CONFIG_KEY_SMTP_AUTH_CODE, "");
|
||||
if (EmailSendUtils.isStrEmpty(sendEmail)) {
|
||||
currentCase.failReason = "INI[EmailConfig]中send_email_account未配置";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + currentCase.failReason);
|
||||
return;
|
||||
}
|
||||
if (EmailSendUtils.isStrEmpty(authCode)) {
|
||||
currentCase.failReason = "INI[EmailConfig]中smtp_auth_code未配置(需QQ邮箱SMTP授权码,非登录密码)";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + currentCase.failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 核心更新:调用无参test(),自动生成测试码,简洁高效
|
||||
// 2. 独立初始化邮件配置
|
||||
LogUtils.d(TAG, "邮件配置未初始化,执行独立初始化");
|
||||
boolean initOk = emailUtils.initEmailConfig();
|
||||
if (!initOk) {
|
||||
currentCase.failReason = "邮件配置初始化失败,排查:1.配置项格式 2.网络连通smtp.qq.com 3.依赖包完整";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + currentCase.failReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 执行核心自测
|
||||
LogUtils.d(TAG, "邮件配置初始化成功,执行自测");
|
||||
boolean testResult = emailUtils.test();
|
||||
if (testResult) {
|
||||
testCaseList.get(4).isPass = true;
|
||||
LogUtils.d(TAG, "【用例结果】selftestmail指令测试通过");
|
||||
currentCase.isPass = true;
|
||||
LogUtils.d(TAG, "【用例结果】selftestmail指令测试通过,自测邮件已发送");
|
||||
} else {
|
||||
testCaseList.get(4).failReason = "邮件发送失败,排查方向:1.授权码正确 2.邮箱开启SMTP 3.网络通畅 4.依赖齐全";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + testCaseList.get(4).failReason);
|
||||
currentCase.failReason = "邮件发送失败,排查方向:1.授权码有效 2.QQ邮箱已开启POP3/SMTP 3.465端口未被拦截 4.Java协议兼容";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试失败:" + currentCase.failReason);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
testCaseList.get(4).failReason = "邮件自测异常:" + e.getMessage() + "(已修复AWT依赖,优先检查邮件配置)";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试异常:" + testCaseList.get(4).failReason, e);
|
||||
currentCase.failReason = "邮件自测执行异常:" + e.getMessage() + "(已兼容Java7+防AWT,优先查配置和网络)";
|
||||
LogUtils.w(TAG, "【用例结果】selftestmail指令测试异常:" + currentCase.failReason, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// 用例05:clearverifycode指令测试(索引同步后移)
|
||||
// 用例06:clearverifycode指令测试
|
||||
testCaseList.add(new TestCase(
|
||||
"clearverifycode指令测试",
|
||||
"验证清空验证码指令能否清空MailAuthUtils缓存",
|
||||
@@ -363,7 +379,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 用例06:testserver指令测试(索引同步后移)
|
||||
// 用例07:testserver指令测试
|
||||
testCaseList.add(new TestCase(
|
||||
"testserver指令测试",
|
||||
"验证服务器连通性测试指令能否正常执行",
|
||||
@@ -383,7 +399,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 用例07:未识别指令测试(索引同步后移)
|
||||
// 用例08:未识别指令测试
|
||||
testCaseList.add(new TestCase(
|
||||
"未识别指令测试",
|
||||
"验证输入未知指令能否正常提示",
|
||||
@@ -402,7 +418,7 @@ public class ConsoleCmdAutoTest {
|
||||
}
|
||||
));
|
||||
|
||||
// 用例08(最后执行):exit指令逻辑测试(索引同步后移)
|
||||
// 用例09(最后执行):exit指令逻辑测试
|
||||
testCaseList.add(new TestCase(
|
||||
"exit指令逻辑测试",
|
||||
"验证exit指令核心处理逻辑有效性",
|
||||
|
||||
@@ -17,9 +17,11 @@ import javax.mail.internet.MimeMessage;
|
||||
* 新增:公开静态空值校验方法 isStrEmpty,供外部类调用
|
||||
* 本次修复:终极规避AWT依赖,配合补丁类彻底解决java.awt.datatransfer.Transferable缺失异常
|
||||
* 新增无参test():自动生成6位测试码,快速自测邮件配置
|
||||
* 补充修复:集成SSL协议兼容配置,解决QQ邮箱465端口SSLHandshakeException
|
||||
* 新增main入口:单一入口调用测试,整合所有测试逻辑
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026-01-15 00:00:00
|
||||
* @LastEditTime 2026-01-26 补全防AWT配置,兼容android-30.jar不冲突
|
||||
* @LastEditTime 2026-01-26 新增main入口+整合测试逻辑,兼容Java7规范
|
||||
*/
|
||||
public class EmailSendUtils {
|
||||
private static final String TAG = "EmailSendUtils";
|
||||
@@ -82,6 +84,11 @@ public class EmailSendUtils {
|
||||
}
|
||||
LogUtils.d(TAG, "initEmailConfig 函数调用,开始初始化邮件配置");
|
||||
|
||||
// ========== 核心修复:SSL协议兼容配置(解决握手失败)- Java7适配 ==========
|
||||
java.security.Security.setProperty("jdk.tls.disabledAlgorithms", "");
|
||||
System.setProperty("https.protocols", "SSLv3,TLSv1,TLSv1.1,TLSv1.2");
|
||||
System.setProperty("javax.net.ssl.trustStoreType", "JKS");
|
||||
|
||||
// 读取INI配置(精简key名,直接关联配置段)
|
||||
String smtpHost = IniConfigUtils.getConfigValue(CONFIG_SECTION_EMAIL, "smtp_host", DEFAULT_SMTP_HOST);
|
||||
String smtpPort = IniConfigUtils.getConfigValue(CONFIG_SECTION_EMAIL, "smtp_port", DEFAULT_SMTP_PORT);
|
||||
@@ -122,11 +129,11 @@ public class EmailSendUtils {
|
||||
try {
|
||||
// 创建邮件会话(匿名内部类,适配Java7)
|
||||
mailSession = Session.getInstance(mailProps, new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(fromEmail, smtpAuthCode);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(fromEmail, smtpAuthCode);
|
||||
}
|
||||
});
|
||||
mailSession.setDebug(false);
|
||||
isConfigInited = true; // 标记初始化完成
|
||||
LogUtils.i(TAG, "邮件发送配置初始化完成,发送方邮箱:" + fromEmail + " 发送方昵称:" + (isEmpty(fromNickname) ? "默认昵称" : fromNickname));
|
||||
@@ -139,34 +146,39 @@ public class EmailSendUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增无参自测】自动生成6位测试码,快速校验邮件配置(最常用)
|
||||
* 无需传参,一键自测,适配快速验证场景
|
||||
* @return 自测成功返回true,失败返回false
|
||||
* 整合型单元测试入口(核心测试函数)
|
||||
* 初始化环境+生成测试码+发送自测邮件,整合所有测试逻辑
|
||||
* @return 测试成功返回true,失败返回false
|
||||
*/
|
||||
public boolean test() {
|
||||
// 自动生成6位纯数字测试码,和验证码规则一致
|
||||
String testCode = String.valueOf((int)(Math.random() * 900000 + 100000));
|
||||
LogUtils.d(TAG, "无参test()调用,自动生成测试码:" + testCode);
|
||||
return test(testCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单元测试专用函数(适配新测试用例,单例调用)
|
||||
* @param testCode 自定义测试码
|
||||
* @return 测试邮件发送成功返回true,否则false
|
||||
*/
|
||||
public boolean test(String testCode) {
|
||||
LogUtils.d(TAG, "test() 函数调用,开始邮件工具类单元测试");
|
||||
// 未初始化则自动执行,失败直接终止
|
||||
if (!isConfigInited) {
|
||||
LogUtils.w(TAG, "邮件会话未初始化,自动执行配置初始化");
|
||||
boolean initResult = initEmailConfig();
|
||||
if (!initResult) {
|
||||
LogUtils.e(TAG, "自动初始化失败,单元测试终止");
|
||||
return false;
|
||||
}
|
||||
// 1. 先初始化INI配置
|
||||
boolean iniInit = IniConfigUtils.init();
|
||||
if (!iniInit) {
|
||||
return false;
|
||||
}
|
||||
return sendSelfTestEmail(testCode);
|
||||
|
||||
LogUtils.d(TAG, "===== 邮件工具类整合单元测试启动 =====");
|
||||
// 1. 初始化配置环境
|
||||
boolean initResult = initEmailConfig();
|
||||
if (!initResult) {
|
||||
LogUtils.e(TAG, "环境初始化失败,单元测试终止");
|
||||
System.err.println("❌ 环境初始化失败!");
|
||||
LogUtils.d(TAG, "===== 邮件工具类整合单元测试失败 =====");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 生成6位测试码
|
||||
String testCode = String.valueOf((int)(Math.random() * 900000 + 100000));
|
||||
LogUtils.d(TAG, "生成测试验证码:" + testCode);
|
||||
|
||||
// 3. 发送自测邮件(核心测试步骤)
|
||||
boolean sendResult = sendSelfTestEmail(testCode);
|
||||
if (sendResult) {
|
||||
LogUtils.i(TAG, "===== 邮件工具类整合单元测试成功 =====");
|
||||
} else {
|
||||
LogUtils.e(TAG, "===== 邮件工具类整合单元测试失败 =====");
|
||||
}
|
||||
return sendResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,9 +317,18 @@ public class EmailSendUtils {
|
||||
fromNickname = null;
|
||||
}
|
||||
|
||||
// ========== 对外提供初始化状态查询(适配测试用例调用) ==========
|
||||
// ========== 对外提供初始化状态查询 ==========
|
||||
public boolean isConfigInited() {
|
||||
return this.isConfigInited;
|
||||
}
|
||||
|
||||
// ========== 程序入口main函数(单一测试入口) ==========
|
||||
public static boolean main(String[] args) {
|
||||
// 调用整合型单元测试,一键完成初始化+自测
|
||||
boolean testResult = EmailSendUtils.getInstance().test();
|
||||
// 退出程序,0=成功,1=失败
|
||||
System.exit(testResult ? 0 : 1);
|
||||
return testResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user