应用签名联网验证模块完成。

This commit is contained in:
2026-01-22 20:41:22 +08:00
parent 323e19a63c
commit 5248ed41bd
3 changed files with 398 additions and 91 deletions

View File

@@ -0,0 +1,100 @@
package cc.winboll.app;
import cc.winboll.LogUtils;
import cc.winboll.util.IniConfigUtils;
import java.util.Base64;
public class AppSignaturesUtils {
// 1. 懒汉单例(解决过早加载问题)
private static AppSignaturesUtils INSTANCE = null;
private static final String CONFIG_SECTION = "APP";
private static final String KEY_SIGN_FINGERPRINT = "app_sign_fingerprint";
private static final String KEY_EFFECTIVE_TIME = "app_sign_effective_time";
private String targetSign;
private long effectiveTime;
// 私有构造
private AppSignaturesUtils() {}
// 2. 懒加载单例+确保配置已加载
public static AppSignaturesUtils getInstance() {
if (INSTANCE == null) {
synchronized (AppSignaturesUtils.class) {
if (INSTANCE == null) {
INSTANCE = new AppSignaturesUtils();
INSTANCE.initConfig(); // 延迟初始化配置
}
}
}
return INSTANCE;
}
// 公共init方法供外部主动初始化可选
public static void init() {
getInstance();
}
private void initConfig() {
try {
this.targetSign = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_SIGN_FINGERPRINT, "").trim();
String timeStr = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_EFFECTIVE_TIME, "0").trim();
this.effectiveTime = Long.parseLong(timeStr);
LogUtils.i("AppSignaturesUtils", "配置读取完成|目标签名:" + targetSign + "|生效时间戳:" + effectiveTime);
} catch (Exception e) {
LogUtils.e("AppSignaturesUtils", "配置读取失败", e);
this.targetSign = "";
this.effectiveTime = 0L;
}
}
public static boolean checksignatures(String signature, long validTime) {
return getInstance().doCheck(signature, validTime);
}
private boolean doCheck(String signature, long validTime) {
if (signature == null || signature.isEmpty() || targetSign.isEmpty() || effectiveTime == 0) {
LogUtils.w("AppSignaturesUtils", "校验失败:签名为空或配置未正确加载");
return false;
}
String decryptedSign;
try {
byte[] signBytes = Base64.getDecoder().decode(signature);
decryptedSign = new String(signBytes, "UTF-8");
} catch (Exception e) {
LogUtils.w("AppSignaturesUtils", "签名解密失败", e);
return false;
}
boolean signMatch = targetSign.equals(decryptedSign);
boolean timeValid = validTime >= effectiveTime;
LogUtils.d("AppSignaturesUtils", "解密后签名:" + decryptedSign + "|签名匹配:" + signMatch + "|时间有效:" + timeValid);
return signMatch && timeValid;
}
public static void main(String[] args) {
test();
}
private static void test() {
IniConfigUtils.init();
LogUtils.init();
AppSignaturesUtils.init();
// 关键修改:手动生成正确签名,彻底杜绝复制粘贴隐形字符
String rawSign = "WinBoLL_AuthCenter_Valid_Sign";
String testSignature = Base64.getEncoder().encodeToString(rawSign.getBytes());
long testValidTime = 1769000000000L;
System.out.println("===== AppSignaturesUtils 单元测试 =====");
System.out.println("原文字符串:" + rawSign);
System.out.println("自动Base64编码后" + testSignature);
System.out.println("示例生效时间戳:" + testValidTime);
boolean result = AppSignaturesUtils.checksignatures(testSignature, testValidTime);
System.out.println("校验结果:" + (result ? "✅ 成功" : "❌ 失败"));
System.out.println("======================================");
}
}

View File

@@ -1,26 +1,35 @@
package cc.winboll.service;
import cc.winboll.LogUtils;
import cc.winboll.util.ServerUtils;
import cc.winboll.util.ConsoleVersionUtils;
import fi.iki.elonen.NanoHTTPD;
import java.io.IOException;
import java.net.SocketException;
import java.util.Map;
import java.util.HashMap;
/**
* 独立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 {
@@ -48,111 +57,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;
}
}

View File

@@ -0,0 +1,231 @@
package cc.winboll.service;
import cc.winboll.LogUtils;
import cc.winboll.app.AppSignaturesUtils;
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请求
* 功能调用AppSignaturesUtils完成校验返回JSON格式校验结果
*/
public NanoHTTPD.Response handleAppSignatureCheck(Map<String, String> params) {
LogUtils.d(TAG, "处理应用签名校验请求,参数:" + params);
// 前置判空
if (params == null) {
LogUtils.w(TAG, "签名校验失败:未获取到任何参数");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
}
// 1. 参数校验signature和validTime必填
String signature = params.get("signature");
String validTimeStr = params.get("validTime");
if (signature == null || signature.isEmpty() || validTimeStr == null || validTimeStr.isEmpty()) {
LogUtils.w(TAG, "签名校验失败参数不全signature或validTime为空");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "参数错误signature和validTime不能为空");
}
// 2. validTime格式校验必须是数字时间戳
long validTime;
try {
validTime = Long.parseLong(validTimeStr);
} catch (NumberFormatException e) {
LogUtils.w(TAG, "签名校验失败validTime格式错误非数字" + validTimeStr);
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "参数错误validTime必须是数字时间戳");
}
// 3. 核心校验逻辑调用AppSignaturesUtils 替代原有硬编码逻辑
boolean isValid = AppSignaturesUtils.checksignatures(signature, validTime);
// 4. Base64解密仅用于日志输出不影响校验逻辑
String decryptedSign = "";
try {
byte[] signBytes = Base64.getDecoder().decode(signature);
decryptedSign = new String(signBytes, "UTF-8");
} catch (Exception e) {
decryptedSign = "解密失败";
}
// 5. 构建响应结果
String msg;
if (isValid) {
msg = "应用签名校验通过,为合法应用";
LogUtils.d(TAG, "签名校验通过:" + msg + ",解密后签名:" + decryptedSign + ",时间戳:" + validTime);
} else {
msg = "应用签名校验失败(签名不匹配或时间不满足生效要求)";
LogUtils.w(TAG, "签名校验失败:" + msg + ",解密后签名:" + decryptedSign + ",时间戳:" + validTime);
}
// 6. 返回JSON响应与APP端预期格式一致字段完整
String responseJson = String.format(
"{\"code\":%d,\"msg\":\"%s\",\"data\":{\"valid\":%b,\"signature\":\"%s\",\"decryptedSign\":\"%s\",\"validTime\":%d}}",
isValid ? 200 : 403,
msg,
isValid,
signature,
decryptedSign,
validTime
);
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, responseJson);
}
}