应用签名联网验证模块完成。
This commit is contained in:
@@ -21,5 +21,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// Gson
|
||||
api 'com.google.code.gson:gson:2.8.9'
|
||||
// Html 解析
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Jan 21 03:11:44 GMT 2026
|
||||
#Thu Jan 22 12:38:42 GMT 2026
|
||||
stageCount=7
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.15
|
||||
publishVersion=15.15.6
|
||||
buildCount=1
|
||||
buildCount=11
|
||||
baseBetaVersion=15.15.7
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
libappbase/src/main/res/xml/network_security_config.xml
Normal file
11
libappbase/src/main/res/xml/network_security_config.xml
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user