添加应用认证实现类源码
This commit is contained in:
@@ -43,6 +43,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':library')
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// Html 解析
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed Jan 14 12:40:12 GMT 2026
|
||||
#Thu Jan 15 02:32:25 GMT 2026
|
||||
stageCount=0
|
||||
libraryProject=
|
||||
libraryProject=library
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=44
|
||||
buildCount=47
|
||||
baseBetaVersion=15.0.1
|
||||
|
||||
74
authcenterapp/src/main/java/cc/winboll/StorageUtils.java
Normal file
74
authcenterapp/src/main/java/cc/winboll/StorageUtils.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package cc.winboll;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import cc.winboll.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 10:10
|
||||
* 本地存储工具类,负责加密私钥等核心数据存储
|
||||
* 存储于应用私有SharedPreferences,仅本应用可访问,适配Android API30
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class StorageUtils {
|
||||
private static final String TAG = "StorageUtils";
|
||||
private static final String SP_NAME = "AuthCenter_Storage";
|
||||
private static final String KEY_ENCRYPT_PRIVATE_KEY = "encrypt_private_key";
|
||||
private static SharedPreferences sp;
|
||||
|
||||
/**
|
||||
* 初始化存储工具
|
||||
*/
|
||||
public static void init(Context context) {
|
||||
if (sp == null && context != null) {
|
||||
sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储加密后的应用私钥
|
||||
*/
|
||||
public static boolean saveEncryptPrivateKey(String encryptKey) {
|
||||
if (sp == null || encryptKey == null) {
|
||||
LogUtils.w(TAG, "存储加密私钥参数无效");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
sp.edit().putString(KEY_ENCRYPT_PRIVATE_KEY, encryptKey).commit();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "存储加密私钥失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储的加密应用私钥
|
||||
*/
|
||||
public static String getEncryptPrivateKey() {
|
||||
if (sp == null) {
|
||||
LogUtils.w(TAG, "存储工具未初始化");
|
||||
return null;
|
||||
}
|
||||
return sp.getString(KEY_ENCRYPT_PRIVATE_KEY, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验存储的加密私钥与目标数据一致
|
||||
*/
|
||||
public static boolean checkEncryptKeyConsistent(String targetKey) {
|
||||
String saveKey = getEncryptPrivateKey();
|
||||
return targetKey != null && targetKey.equals(saveKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有存储数据
|
||||
*/
|
||||
public static void clearAllData() {
|
||||
if (sp != null) {
|
||||
sp.edit().clear().commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package cc.winboll.manager;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import cc.winboll.model.AuthDataModel;
|
||||
import cc.winboll.utils.AuthUtils;
|
||||
import cc.winboll.utils.ServerUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 10:14
|
||||
* @Describe 心跳保活管理类,负责定时发送PING请求,处理PONG响应
|
||||
* 适配Java8,每分钟发送1次心跳,支持异常重试
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class HeartbeatManager {
|
||||
private static final String TAG = "HeartbeatManager";
|
||||
private static final long HEARTBEAT_INTERVAL = 60 * 1000; // 1分钟
|
||||
private static Timer heartbeatTimer;
|
||||
private static String serverPublicKey; // 服务器公钥,用于加密PING数据
|
||||
|
||||
/**
|
||||
* 初始化心跳管理器
|
||||
*/
|
||||
public static void init(String serverPubKey) {
|
||||
serverPublicKey = serverPubKey;
|
||||
LogUtils.d(TAG, "心跳管理器初始化完成,间隔:" + HEARTBEAT_INTERVAL + "ms");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳任务
|
||||
*/
|
||||
public static void startHeartbeat() {
|
||||
stopHeartbeat(); // 先停止已有任务,避免重复
|
||||
heartbeatTimer = new Timer();
|
||||
heartbeatTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendHeartbeatPing();
|
||||
}
|
||||
}, 0, HEARTBEAT_INTERVAL);
|
||||
LogUtils.i(TAG, "心跳保活任务已启动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳任务
|
||||
*/
|
||||
public static void stopHeartbeat() {
|
||||
if (heartbeatTimer != null) {
|
||||
heartbeatTimer.cancel();
|
||||
heartbeatTimer = null;
|
||||
LogUtils.i(TAG, "心跳保活任务已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳PING请求
|
||||
*/
|
||||
private static void sendHeartbeatPing() {
|
||||
if (!AuthUtils.isLogin() || serverPublicKey == null) {
|
||||
LogUtils.w(TAG, "未登录或服务器公钥未初始化,跳过心跳");
|
||||
return;
|
||||
}
|
||||
// 组装PING数据
|
||||
AuthDataModel pingModel = new AuthDataModel();
|
||||
pingModel.setAppId(AuthUtils.getAppId());
|
||||
pingModel.setShortToken(AuthUtils.getShortToken());
|
||||
pingModel.setLoginFlag(true);
|
||||
String pingJson = pingModel.buildPingJson();
|
||||
if (pingJson == null) {
|
||||
LogUtils.w(TAG, "PING数据组装失败");
|
||||
return;
|
||||
}
|
||||
// 服务器公钥加密PING数据
|
||||
String encryptPing = AuthUtils.encryptByPublicKey(pingJson, serverPublicKey);
|
||||
if (encryptPing == null) {
|
||||
LogUtils.w(TAG, "PING数据加密失败");
|
||||
return;
|
||||
}
|
||||
// 发送请求
|
||||
String response = ServerUtils.sendPingRequest(encryptPing);
|
||||
if (response != null) {
|
||||
handlePongResponse(response);
|
||||
} else {
|
||||
LogUtils.w(TAG, "心跳PING请求失败,下次重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理PONG响应
|
||||
*/
|
||||
private static void handlePongResponse(String encryptPongData) {
|
||||
// 临时私钥解密PONG数据(需确保临时私钥已存在)
|
||||
String pongJson = AuthUtils.decryptByPrivateKey(encryptPongData, AuthUtils.getTempPrivateKey().toString());
|
||||
AuthDataModel pongModel = AuthDataModel.parsePongJson(pongJson);
|
||||
if (pongModel == null) {
|
||||
LogUtils.w(TAG, "PONG数据解析失败");
|
||||
return;
|
||||
}
|
||||
// 更新Token
|
||||
if (pongModel.isLoginFlag() && pongModel.getShortToken() != null) {
|
||||
AuthUtils.setShortToken(pongModel.getShortToken());
|
||||
LogUtils.d(TAG, "心跳保活成功,Token已更新");
|
||||
} else {
|
||||
LogUtils.w(TAG, "登录态失效,停止心跳");
|
||||
AuthUtils.setLoginStatus(false);
|
||||
stopHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
library/.gitignore
vendored
Normal file
1
library/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
25
library/build.gradle
Normal file
25
library/build.gradle
Normal file
@@ -0,0 +1,25 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply from: '../.winboll/winboll_lib_build.gradle'
|
||||
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
8
library/build.properties
Normal file
8
library/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Thu Jan 15 02:32:25 GMT 2026
|
||||
stageCount=0
|
||||
libraryProject=library
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.0
|
||||
buildCount=47
|
||||
baseBetaVersion=15.0.1
|
||||
17
library/proguard-rules.pro
vendored
Normal file
17
library/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
9
library/src/main/AndroidManifest.xml
Normal file
9
library/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.library" >
|
||||
|
||||
<application>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
91
library/src/main/java/cc/winboll/LogUtils.java
Normal file
91
library/src/main/java/cc/winboll/LogUtils.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package cc.winboll;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* 日志工具类,对接Java原生java.util.logging,简化分级调用+统一格式化输出
|
||||
* 适配Java8语法,兼容Android API30,支持异常堆栈打印
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/14 00:00:00
|
||||
* @LastEditTime 2026/01/15 02:42:35
|
||||
*/
|
||||
public class LogUtils {
|
||||
// 全局日志实例,绑定当前工具类名
|
||||
private static final Logger LOGGER = Logger.getLogger(LogUtils.class.getName());
|
||||
|
||||
// 静态代码块初始化日志配置(仅执行一次,全局生效)
|
||||
static {
|
||||
// 关闭父处理器,避免日志重复输出
|
||||
LOGGER.setUseParentHandlers(false);
|
||||
// 自定义控制台输出处理器,绑定格式化规则
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setFormatter(new CustomLogFormatter());
|
||||
LOGGER.addHandler(consoleHandler);
|
||||
// 默认日志级别(全量输出,可按需调整屏蔽低级别日志)
|
||||
LOGGER.setLevel(Level.ALL);
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
}
|
||||
|
||||
// 私有化构造方法,禁止外部实例化
|
||||
private LogUtils() {}
|
||||
|
||||
// 调试日志(细粒度开发调试信息,上线可屏蔽)
|
||||
public static void d(String tag, String msg) {
|
||||
LOGGER.fine(String.format("[%s] %s", tag, msg));
|
||||
}
|
||||
|
||||
// 调试日志-重载版(带异常堆栈,便于调试阶段排查问题)
|
||||
public static void d(String tag, String msg, Throwable throwable) {
|
||||
LOGGER.log(Level.FINE, String.format("[%s] %s", tag, msg), throwable);
|
||||
}
|
||||
|
||||
// 信息日志(常规运行状态、流程节点信息)
|
||||
public static void i(String tag, String msg) {
|
||||
LOGGER.info(String.format("[%s] %s", tag, msg));
|
||||
}
|
||||
|
||||
// 信息日志-重载版(带异常堆栈,关联信息类场景的异常链路)
|
||||
public static void i(String tag, String msg, Throwable throwable) {
|
||||
LOGGER.log(Level.INFO, String.format("[%s] %s", tag, msg), throwable);
|
||||
}
|
||||
|
||||
// 警告日志-基础版(非致命异常、潜在风险提示)
|
||||
public static void w(String tag, String msg) {
|
||||
LOGGER.warning(String.format("[%s] %s", tag, msg));
|
||||
}
|
||||
|
||||
// 警告日志-重载版(带异常堆栈,便于排查警告根源)
|
||||
public static void w(String tag, String msg, Throwable throwable) {
|
||||
LOGGER.log(Level.WARNING, String.format("[%s] %s", tag, msg), throwable);
|
||||
}
|
||||
|
||||
// 错误日志-基础版(致命异常、核心流程错误提示)
|
||||
public static void e(String tag, String msg) {
|
||||
LOGGER.severe(String.format("[%s] %s", tag, msg));
|
||||
}
|
||||
|
||||
// 错误日志-重载版(带异常堆栈,定位错误完整链路)
|
||||
public static void e(String tag, String msg, Throwable throwable) {
|
||||
LOGGER.log(Level.SEVERE, String.format("[%s] %s", tag, msg), throwable);
|
||||
}
|
||||
|
||||
// 自定义日志格式化器,统一输出格式,提升可读性
|
||||
static class CustomLogFormatter extends Formatter {
|
||||
@Override
|
||||
public String format(LogRecord record) {
|
||||
// 输出格式:[时间戳] [日志级别] [线程名] [日志源] - 日志内容
|
||||
return String.format("[%1$tF %1$tT] [%2$s] [%3$s] %4$s - %5$s%n",
|
||||
new Date(record.getMillis()),
|
||||
record.getLevel().getName(),
|
||||
Thread.currentThread().getName(),
|
||||
record.getLoggerName(),
|
||||
formatMessage(record));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
library/src/main/java/cc/winboll/model/AuthDataModel.java
Normal file
64
library/src/main/java/cc/winboll/model/AuthDataModel.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package cc.winboll.model;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import cc.winboll.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 10:11
|
||||
* @Describe 鉴权核心数据模型,封装PING/PONG JSON数据,负责序列化与反序列化
|
||||
* 适配Java8,支持核心字段解析与组装
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class AuthDataModel {
|
||||
private static final String TAG = "AuthDataModel";
|
||||
// PING/PONG 核心字段
|
||||
private String appId;
|
||||
private String shortToken;
|
||||
private boolean loginFlag;
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
public String getShortToken() { return shortToken; }
|
||||
public void setShortToken(String shortToken) { this.shortToken = shortToken; }
|
||||
public boolean isLoginFlag() { return loginFlag; }
|
||||
public void setLoginFlag(boolean loginFlag) { this.loginFlag = loginFlag; }
|
||||
|
||||
/**
|
||||
* 组装PING JSON字符串
|
||||
*/
|
||||
public String buildPingJson() {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("appId", appId);
|
||||
json.put("shortToken", shortToken);
|
||||
json.put("loginFlag", loginFlag);
|
||||
return json.toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "组装PING JSON失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析PONG JSON字符串
|
||||
*/
|
||||
public static AuthDataModel parsePongJson(String jsonStr) {
|
||||
if (jsonStr == null || jsonStr.isEmpty()) {
|
||||
LogUtils.w(TAG, "PONG JSON字符串为空");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSONObject json = new JSONObject(jsonStr);
|
||||
AuthDataModel model = new AuthDataModel();
|
||||
model.setAppId(json.optString("appId"));
|
||||
model.setShortToken(json.optString("shortToken"));
|
||||
model.setLoginFlag(json.optBoolean("loginFlag"));
|
||||
return model;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "解析PONG JSON失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
111
library/src/main/java/cc/winboll/utils/AuthUtils.java
Normal file
111
library/src/main/java/cc/winboll/utils/AuthUtils.java
Normal file
@@ -0,0 +1,111 @@
|
||||
package cc.winboll.utils;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
/**
|
||||
* 鉴权核心工具类,负责密钥生成、加解密、登录态管理
|
||||
* 适配Java8,非对称加密采用RSA 2048位
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 09:56:00
|
||||
*/
|
||||
public class AuthUtils {
|
||||
private static final String TAG = "AuthUtils";
|
||||
private static final String ALGORITHM = "RSA";
|
||||
private static final int KEY_SIZE = 2048;
|
||||
// 临时存储明文私钥,使用后立即置空
|
||||
private static PrivateKey tempPrivateKey;
|
||||
// 登录态核心参数
|
||||
private static String appId;
|
||||
private static String shortToken;
|
||||
private static boolean loginStatus = false;
|
||||
|
||||
/**
|
||||
* 生成RSA密钥对(公钥+私钥)
|
||||
*/
|
||||
public static KeyPair generateRsaKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
|
||||
keyGen.initialize(KEY_SIZE);
|
||||
return keyGen.generateKeyPair();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "生成RSA密钥对失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 公钥加密数据
|
||||
*/
|
||||
public static String encryptByPublicKey(String data, String publicKeyStr) {
|
||||
try {
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
|
||||
PublicKey publicKey = java.security.KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
byte[] encryptBytes = cipher.doFinal(data.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(encryptBytes);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "公钥加密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 私钥解密数据
|
||||
*/
|
||||
public static String decryptByPrivateKey(String data, String privateKeyStr) {
|
||||
try {
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyStr);
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
PrivateKey privateKey = java.security.KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(data));
|
||||
return new String(decryptBytes, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "私钥解密失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时明文私钥
|
||||
*/
|
||||
public static PrivateKey getTempPrivateKey() {
|
||||
return tempPrivateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时存储明文私钥,用完需手动置空
|
||||
*/
|
||||
public static void setTempPrivateKey(PrivateKey privateKey) {
|
||||
tempPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空临时私钥,保障安全
|
||||
*/
|
||||
public static void clearTempPrivateKey() {
|
||||
tempPrivateKey = null;
|
||||
}
|
||||
|
||||
// 登录态相关get/set方法
|
||||
public static String getAppId() { return appId; }
|
||||
public static void setAppId(String appId) { AuthUtils.appId = appId; }
|
||||
public static String getShortToken() { return shortToken; }
|
||||
public static void setShortToken(String shortToken) { AuthUtils.shortToken = shortToken; }
|
||||
public static boolean isLogin() { return loginStatus; }
|
||||
public static void setLoginStatus(boolean status) { loginStatus = status; }
|
||||
}
|
||||
|
||||
124
library/src/main/java/cc/winboll/utils/NFCUtils.java
Normal file
124
library/src/main/java/cc/winboll/utils/NFCUtils.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package cc.winboll.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.nfc.FormatException;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.Ndef;
|
||||
import android.nfc.tech.NdefFormatable;
|
||||
import cc.winboll.LogUtils;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 10:06
|
||||
* NFC操作工具类,负责NFC功能检测、卡连接、数据读写
|
||||
* 适配Android API30,仅支持空白NFC卡操作
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class NFCUtils {
|
||||
private static final String TAG = "NFCUtils";
|
||||
|
||||
/**
|
||||
* 检测手机NFC功能是否开启
|
||||
*/
|
||||
public static boolean isNfcEnabled(Context context) {
|
||||
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(context);
|
||||
return nfcAdapter != null && nfcAdapter.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为空白NFC卡(无NDEF数据)
|
||||
*/
|
||||
public static boolean isBlankNfcCard(Tag tag) {
|
||||
try {
|
||||
Ndef ndef = Ndef.get(tag);
|
||||
if (ndef != null) {
|
||||
return ndef.getCachedNdefMessage() == null || ndef.getCachedNdefMessage().getRecords().length == 0;
|
||||
}
|
||||
NdefFormatable ndefFormatable = NdefFormatable.get(tag);
|
||||
return ndefFormatable != null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.w(TAG, "检测NFC卡是否空白失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向NFC卡写入加密数据(仅支持NDEF格式)
|
||||
*/
|
||||
public static boolean writeDataToNfc(Tag tag, String encryptData) {
|
||||
if (tag == null || encryptData == null || encryptData.isEmpty()) {
|
||||
LogUtils.w(TAG, "NFC写入参数无效");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Ndef ndef = Ndef.get(tag);
|
||||
if (ndef != null) {
|
||||
ndef.connect();
|
||||
if (!ndef.isWritable()) {
|
||||
LogUtils.w(TAG, "NFC卡不可写入");
|
||||
return false;
|
||||
}
|
||||
android.nfc.NdefRecord record = new android.nfc.NdefRecord(
|
||||
android.nfc.NdefRecord.TNF_WELL_KNOWN,
|
||||
android.nfc.NdefRecord.RTD_TEXT,
|
||||
new byte[0],
|
||||
encryptData.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
android.nfc.NdefMessage message = new android.nfc.NdefMessage(new android.nfc.NdefRecord[]{record});
|
||||
int size = message.toByteArray().length;
|
||||
if (ndef.getMaxSize() < size) {
|
||||
LogUtils.w(TAG, "NFC卡存储空间不足");
|
||||
return false;
|
||||
}
|
||||
ndef.writeNdefMessage(message);
|
||||
ndef.close();
|
||||
return true;
|
||||
} else {
|
||||
NdefFormatable ndefFormatable = NdefFormatable.get(tag);
|
||||
if (ndefFormatable != null) {
|
||||
ndefFormatable.connect();
|
||||
android.nfc.NdefRecord record = new android.nfc.NdefRecord(
|
||||
android.nfc.NdefRecord.TNF_WELL_KNOWN,
|
||||
android.nfc.NdefRecord.RTD_TEXT,
|
||||
new byte[0],
|
||||
encryptData.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
android.nfc.NdefMessage message = new android.nfc.NdefMessage(new android.nfc.NdefRecord[]{record});
|
||||
ndefFormatable.format(message);
|
||||
ndefFormatable.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (FormatException|IOException e) {
|
||||
LogUtils.e(TAG, "NFC卡写入失败", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从NFC卡读取数据
|
||||
*/
|
||||
public static String readDataFromNfc(Tag tag) {
|
||||
if (tag == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Ndef ndef = Ndef.get(tag);
|
||||
if (ndef != null) {
|
||||
ndef.connect();
|
||||
android.nfc.NdefMessage message = ndef.getCachedNdefMessage();
|
||||
ndef.close();
|
||||
if (message != null && message.getRecords().length > 0) {
|
||||
return new String(message.getRecords()[0].getPayload(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "NFC卡读取失败", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
129
library/src/main/java/cc/winboll/utils/ServerUtils.java
Normal file
129
library/src/main/java/cc/winboll/utils/ServerUtils.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package cc.winboll.utils;
|
||||
|
||||
import cc.winboll.LogUtils;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/15 10:08
|
||||
* 服务器交互工具类,负责请求封装、接口调用、响应处理
|
||||
* 适配Java8,支持GET/POST请求,兼容Android与标准JVM
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
*/
|
||||
public class ServerUtils {
|
||||
private static final String TAG = "ServerUtils";
|
||||
private static final int CONNECT_TIMEOUT = 5000;
|
||||
private static final int READ_TIMEOUT = 5000;
|
||||
private static String serverBaseUrl; // 服务器基础地址
|
||||
|
||||
/**
|
||||
* 初始化服务器基础地址
|
||||
*/
|
||||
public static void initServerUrl(String url) {
|
||||
serverBaseUrl = url;
|
||||
LogUtils.d(TAG, "服务器地址初始化完成:" + serverBaseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码请求(POST)
|
||||
*/
|
||||
public static String sendVerifyCode(String email) {
|
||||
String url = serverBaseUrl + "/api/sendVerifyCode";
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("email", email);
|
||||
return sendPostRequest(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码请求(POST)
|
||||
*/
|
||||
public static String verifyCode(String email, String code) {
|
||||
String url = serverBaseUrl + "/api/verifyCode";
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("email", email);
|
||||
params.put("code", code);
|
||||
return sendPostRequest(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交应用公钥请求(POST)
|
||||
*/
|
||||
public static String submitAppPublicKey(String email, String publicKey) {
|
||||
String url = serverBaseUrl + "/api/submitPublicKey";
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("email", email);
|
||||
params.put("appPublicKey", publicKey);
|
||||
return sendPostRequest(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳PING请求(POST,加密数据)
|
||||
*/
|
||||
public static String sendPingRequest(String encryptPingData) {
|
||||
String url = serverBaseUrl + "/api/heartbeat/ping";
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("encryptData", encryptPingData);
|
||||
return sendPostRequest(url, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用POST请求封装
|
||||
*/
|
||||
private static String sendPostRequest(String urlStr, Map<String, String> params) {
|
||||
if (urlStr == null || params == null) {
|
||||
LogUtils.w(TAG, "POST请求参数为空");
|
||||
return null;
|
||||
}
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(urlStr);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
// 拼接参数
|
||||
StringBuilder paramStr = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
paramStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
|
||||
}
|
||||
if (paramStr.length() > 0) {
|
||||
paramStr.deleteCharAt(paramStr.length() - 1);
|
||||
}
|
||||
|
||||
OutputStream os = conn.getOutputStream();
|
||||
os.write(paramStr.toString().getBytes("UTF-8"));
|
||||
os.flush();
|
||||
os.close();
|
||||
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
br.close();
|
||||
return response.toString();
|
||||
} else {
|
||||
LogUtils.w(TAG, "POST请求失败,响应码:" + conn.getResponseCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "POST请求异常", e);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
7
library/src/main/res/values/strings.xml
Normal file
7
library/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="lib_name">library</string>
|
||||
<string name="hello_world">Hello world!</string>
|
||||
|
||||
</resources>
|
||||
5
library/src/main/res/values/styles.xml
Normal file
5
library/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="@android:style/Theme.Holo.Light">
|
||||
</style>
|
||||
</resources>
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
// AuthCenterAPP 项目编译设置
|
||||
//include ':authcenterapp'
|
||||
//include ':library'
|
||||
//rootProject.name = "authcenterapp"
|
||||
|
||||
// Contacts 项目编译设置
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Tue Jan 13 16:53:24 HKT 2026
|
||||
#Tue Jan 13 18:37:44 GMT 2026
|
||||
stageCount=11
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.10
|
||||
buildCount=0
|
||||
buildCount=1
|
||||
baseBetaVersion=15.11.11
|
||||
|
||||
@@ -11,5 +11,11 @@
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<!-- 允许特定域名的明文请求(仅本地127.0.0.1) -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user