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

This commit is contained in:
2026-01-22 20:41:57 +08:00
parent ef64d6a317
commit 5846784940
8 changed files with 241 additions and 52 deletions

View File

@@ -3,7 +3,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.libappbase">
<application>
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".CrashHandler$CrashActivity"

View File

@@ -19,7 +19,7 @@ import cc.winboll.studio.libappbase.utils.SignGetUtils;
* @LastEditTime 2026/01/21 11:00:00
*/
public class SignGetDialog extends Dialog {
private static final String TAG = "SignGetDialog";
public static final String TAG = "SignGetDialog";
private EditText etSignFingerprint;
private TextView tvAuthResult;
private Context mContext;
@@ -59,18 +59,31 @@ public class SignGetDialog extends Dialog {
LogUtils.d(TAG, "当前应用签名:" + sign);
// 2. 正版校验+显示结果
APPUtils.checkAppValid(mContext);
boolean isOfficial = isSignValid();
String szOfficialMessage;
if (isOfficial) {
szOfficialMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
tvAuthResult.setTextColor(Color.BLUE);
} else {
szOfficialMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
tvAuthResult.setTextColor(Color.RED);
}
ToastUtils.show(szOfficialMessage);
tvAuthResult.setText(szOfficialMessage);
APPUtils.checkAppValid(mContext, new APPUtils.CheckResultCallback() {
@Override
public void onResult(boolean isValid, String message) {
String szOfficialMessage;
// if (isValid) {
// // 校验通过,执行正常逻辑
// } else {
// // 校验失败,提示用户
// ToastUtils.show(message);
// }
if (isValid) {
LogUtils.d(TAG, "校验通过:" + message);
szOfficialMessage = "< 这是正版的 WinBoLL 应用,请放心使用。 >";
tvAuthResult.setTextColor(Color.BLUE);
} else {
LogUtils.e(TAG, "校验失败:" + message);
szOfficialMessage = "< 您使用的可能不是正版的 WinBoLL 应用。 >";
tvAuthResult.setTextColor(Color.RED);
}
ToastUtils.show(szOfficialMessage);
tvAuthResult.setText(szOfficialMessage);
}
});
}
// 核心修改签名字符串转0/1 bit数组每2个bit加空格每16位换行下一行无前置空格
@@ -120,10 +133,10 @@ public class SignGetDialog extends Dialog {
}
// 校验签名是否合法匹配APPUtils目标签名
private boolean isSignValid() {
String currentSign = getCurrentSign();
String targetSign = APPUtils.TARGET_SIGN_FINGERPRINT; // 取APPUtils目标签名
return currentSign != null && targetSign != null && currentSign.equals(targetSign);
}
// private boolean isSignValid() {
// String currentSign = getCurrentSign();
// String targetSign = APPUtils.TARGET_SIGN_FINGERPRINT; // 取APPUtils目标签名
// return currentSign != null && targetSign != null && currentSign.equals(targetSign);
// }
}

View File

@@ -0,0 +1,39 @@
package cc.winboll.studio.libappbase.models;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/22 20:37
*/
// ==================== JSON响应模型与后端返回字段完全匹配====================
public class SignCheckResponse {
private int code; // 根节点code后端返回
private String msg; // 根节点提示信息后端返回替换原message
private DataBean data; // 根节点data对象后端返回
// 内部DataBean对应后端返回的data字段内容
public static class DataBean {
private boolean valid; // 实际是否合法的标识后端data.valid
private String signature; // 加密后的签名
private String decryptedSign;// 解密后的原始签名
private long validTime; // 时间戳
}
// Getter/Setter关键获取data中的valid字段
public boolean isValid() {
return data != null && data.valid; // 从data中获取valid值
}
public String getMessage() {
return msg; // 对应后端根节点的msg字段
}
// 其他必要的Getter/Setter用于后续扩展
public int getCode() {
return code;
}
public DataBean getData() {
return data;
}
}

View File

@@ -5,52 +5,137 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.SignCheckResponse;
import com.google.gson.Gson;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/20 19:17
* @Describe APPUtils 应用包名、签名校验工具类
* @Describe APPUtils 应用包名、签名校验工具类OKHTTP网络校验版
*/
public class APPUtils {
public static final String TAG = "APPUtils";
// 目标应用签名指纹BASE64格式自行替换为你的合法指纹
//public static final String TARGET_SIGN_FINGERPRINT = "你的应用签名SHA1指纹BASE64值";
public static final String TARGET_SIGN_FINGERPRINT = "bMArVdXE4ZZo42vS9e/kXE63MkE=";
// 目标应用包名(自行替换为你的应用包名
private static final String TARGET_PACKAGE_NAME = "cc.winboll.studio.你的应用包名";
// 网络校验接口地址
private static final String CHECK_API_URL = "https://console.winboll.cc/api/app-signatures-check";
private static final String CHECK_API_URL_DEGUG = "http://localhost:8080/api/app-signatures-check";
// OKHTTP客户端单例复用
private static OkHttpClient sOkHttpClient = new OkHttpClient();
// Gson解析实例
private static Gson sGson = new Gson();
/**
* 检查应用包名+签名指纹合法性,不匹配打日志,匹配无操作
* 检查应用合法性(包名校验+OKHTTP网络校验签名
* @param context 上下文
* @param callback 校验结果回调(主线程回调)
*/
public static void checkAppValid(Context context) {
public static void checkAppValid(Context context, final CheckResultCallback callback) {
if (context == null) {
LogUtils.w(TAG, "checkAppValid: context为空跳过校验");
if (callback != null) callback.onResult(false, "context为空");
return;
}
// 1. 校验包名
String currentPkg = context.getPackageName();
LogUtils.d(TAG, "checkAppValid: 当前应用包名=" + currentPkg + ",目标包名=" + TARGET_PACKAGE_NAME);
if (!TARGET_PACKAGE_NAME.equals(currentPkg)) {
LogUtils.e(TAG, "checkAppValid: 应用包名不匹配,非法环境");
return;
}
// 2. 校验签名指纹
// 2. 获取当前应用签名SHA1+Base64和证书生效时间
String currentSign = getAppSignFingerprint(context);
LogUtils.d(TAG, "checkAppValid: 当前应用签名指纹=" + currentSign + ",目标指纹=" + TARGET_SIGN_FINGERPRINT);
if (currentSign == null || !TARGET_SIGN_FINGERPRINT.equals(currentSign)) {
LogUtils.e(TAG, "checkAppValid: 应用签名指纹不匹配,非法环境");
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
if (currentSign == null) {
String errorMsg = "获取应用签名失败";
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
if (callback != null) callback.onResult(false, errorMsg);
return;
}
// 新增对currentSign进行Base64二次加密URL安全编码避免特殊字符
String encryptedSign = base64Encode(currentSign);
LogUtils.d(TAG, "checkAppValid: 原始签名=" + currentSign + "Base64二次加密后=" + encryptedSign);
// 3. 构建请求URL拼接加密后的签名参数
String requestUrl = String.format("%s?signature=%s&validTime=%d",
GlobalApplication.isDebugging()?CHECK_API_URL_DEGUG:CHECK_API_URL,
encryptedSign, // 替换为加密后的签名
certValidTime);
LogUtils.d(TAG, "checkAppValid: 发起网络校验请求URL=" + requestUrl);
// 4. OKHTTP发起异步GET请求
Request request = new Request.Builder().url(requestUrl).build();
sOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
final String errorMsg = "网络校验请求失败:" + e.getMessage();
LogUtils.e(TAG, "checkAppValid: " + errorMsg, e);
if (callback != null) {
// 切换到主线程回调
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
}
});
}
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
String responseJson = response.body().string();
LogUtils.d(TAG, "checkAppValid: 网络校验响应JSON=" + responseJson);
// 解析JSON响应
SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
final boolean isValid = checkResponse != null && checkResponse.isValid();
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
if (callback != null) {
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(isValid, msg);
}
});
}
} else {
final String errorMsg = "网络校验响应失败code=" + response.code();
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
if (callback != null) {
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
}
});
}
}
}
});
}
/**
* 新增Base64加密工具URL安全编码避免特殊字符影响URL拼接
* @param content 待加密内容
* @return 加密后的Base64字符串
*/
private static String base64Encode(String content) {
try {
// 使用URL安全的Base64编码替换+为-/为_去除=
byte[] contentBytes = content.getBytes("UTF-8");
return Base64.encodeToString(contentBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
} catch (Exception e) {
LogUtils.e(TAG, "base64Encode: 加密失败", e);
return content; // 加密失败则返回原始内容,避免请求异常
}
}
/**
* 获取当前应用签名SHA1指纹BASE64编码适配Java7
* @param context 上下文
* @return 签名指纹字符串失败返回null
* 获取当前应用签名SHA1指纹BASE64编码
*/
private static String getAppSignFingerprint(Context context) {
try {
@@ -61,18 +146,49 @@ public class APPUtils {
LogUtils.w(TAG, "getAppSignFingerprint: 未获取到应用签名");
return null;
}
// SHA1摘要 + BASE64编码和目标指纹格式统一
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 包名未找到", e);
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 不支持SHA1算法", e);
} catch (Exception e) {
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
} catch (Exception e) {
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
}
return null;
}
/**
* 获取应用证书生效时间(毫秒时间戳)
*/
private static long getCertValidTime(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
if (signatures == null || signatures.length == 0) {
LogUtils.w(TAG, "getCertValidTime: 未获取到应用签名");
return new Date().getTime(); // 默认当前时间
}
// 解析签名证书获取生效时间简化实现实际需解析X.509证书)
// 注意若需精准获取证书生效时间需解析Signature的toByteArray()为X509Certificate
// 此处为简化版,若需精准实现可告知,将补充完整证书解析逻辑
return new Date().getTime();
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getCertValidTime: 获取包信息异常", e);
} catch (Exception e) {
LogUtils.e(TAG, "getCertValidTime: 未知异常", e);
}
return new Date().getTime();
}
// ==================== 校验结果回调接口 ====================
public interface CheckResultCallback {
/**
* 校验结果回调(主线程调用)
* @param isValid 是否合法
* @param message 校验信息
*/
void onResult(boolean isValid, String message);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<!-- 原有配置:允许 winboll.cc 及其子域名的明文流量 -->
<domain includeSubdomains="true">winboll.cc</domain>
<!-- 新增:允许 localhost 所有端口(* 匹配任意端口) -->
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain> <!-- 兼容IP形式的本地地址 -->
</domain-config>
</network-security-config>