feat(client): 实现稳定的设备ID生成与认证优化
- 重构设备ID生成逻辑,采用多重降级策略确保唯一性与稳定性- 移除客户端SQLite缓存依赖,改用localStorage存储token与设备ID - 优化认证流程,简化token管理与会话恢复逻辑- 增加设备数量限制检查,防止超出配额 - 更新SSE连接逻辑,适配新的认证机制 - 调整Redis连接池配置,提升并发性能与稳定性 - 移除冗余的缓存接口与本地退出逻辑 - 修复设备移除时的状态处理问题,避免重复调用offline接口 - 引入OSHI库用于硬件信息采集(备用方案)- 更新开发环境API地址配置
This commit is contained in:
@@ -31,7 +31,6 @@ public class ErrorReportAspect {
|
||||
// 检查返回值是否表示错误
|
||||
if (result instanceof JsonData) {
|
||||
JsonData jsonData = (JsonData) result;
|
||||
// code != 0 表示失败(根据JsonData注释:0表示成功,-1表示失败,1表示处理中)
|
||||
if (jsonData.getCode() != null && jsonData.getCode() != 0) {
|
||||
// 创建一个RuntimeException来包装错误信息
|
||||
String errorMsg = jsonData.getMsg() != null ? jsonData.getMsg() : "未知错误";
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.tashow.erp.entity.AuthTokenEntity;
|
||||
import com.tashow.erp.entity.CacheDataEntity;
|
||||
import com.tashow.erp.repository.AuthTokenRepository;
|
||||
import com.tashow.erp.repository.CacheDataRepository;
|
||||
import com.tashow.erp.service.IAuthService;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 客户端本地服务控制器
|
||||
@@ -20,21 +14,6 @@ import java.util.Optional;
|
||||
public class AuthController {
|
||||
@Autowired
|
||||
private AuthTokenRepository authTokenRepository;
|
||||
@Autowired
|
||||
private CacheDataRepository cacheDataRepository;
|
||||
|
||||
/**
|
||||
* 退出登录(清理本地状态)
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<?> logout(@RequestBody Map<String, Object> data) {
|
||||
// 清理本地缓存
|
||||
try {
|
||||
cacheDataRepository.deleteByCacheKey("token");
|
||||
cacheDataRepository.deleteByCacheKey("deviceId");
|
||||
} catch (Exception ignored) {}
|
||||
return ResponseEntity.ok(Map.of("code", 0, "message", "退出成功"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存认证密钥
|
||||
@@ -51,7 +30,6 @@ public class AuthController {
|
||||
return JsonData.buildSuccess("认证信息保存成功");
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/auth/get")
|
||||
public JsonData getAuth(@RequestParam String serviceName) {
|
||||
return JsonData.buildSuccess(authTokenRepository.findByServiceName(serviceName).map(AuthTokenEntity::getToken).orElse(null));
|
||||
@@ -66,69 +44,6 @@ public class AuthController {
|
||||
return JsonData.buildSuccess("认证信息删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存缓存数据
|
||||
*/
|
||||
@PostMapping("/cache/save")
|
||||
public JsonData saveCache(@RequestBody Map<String, Object> data) {
|
||||
String key = (String) data.get("key");
|
||||
String value = (String) data.get("value");
|
||||
if (key == null || value == null) return JsonData.buildError("key和value不能为空");
|
||||
CacheDataEntity entity = cacheDataRepository.findByCacheKey(key).orElse(new CacheDataEntity());
|
||||
entity.setCacheKey(key);
|
||||
entity.setCacheValue(value);
|
||||
cacheDataRepository.save(entity);
|
||||
|
||||
return JsonData.buildSuccess("缓存数据保存成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存数据
|
||||
*/
|
||||
@GetMapping("/cache/get")
|
||||
public JsonData getCache(@RequestParam String key) {
|
||||
return JsonData.buildSuccess(cacheDataRepository.findByCacheKey(key)
|
||||
.map(CacheDataEntity::getCacheValue).orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存数据
|
||||
*/
|
||||
@DeleteMapping("/cache/remove")
|
||||
public JsonData removeCache(@RequestParam String key) {
|
||||
cacheDataRepository.findByCacheKey(key).ifPresent(cacheDataRepository::delete);
|
||||
return JsonData.buildSuccess("缓存数据删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存数据
|
||||
*/
|
||||
@PostMapping("/cache/delete")
|
||||
public JsonData deleteCacheByPost(@RequestParam String key) {
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
return JsonData.buildError("key不能为空");
|
||||
}
|
||||
System.out.println("key: " + key);
|
||||
cacheDataRepository.deleteByCacheKey(key);
|
||||
return JsonData.buildSuccess("缓存数据删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话引导:检查SQLite中是否存在token
|
||||
*/
|
||||
@GetMapping("/session/bootstrap")
|
||||
public JsonData sessionBootstrap() {
|
||||
Optional<CacheDataEntity> tokenEntity = cacheDataRepository.findByCacheKey("token");
|
||||
if (tokenEntity.isEmpty()) {
|
||||
return JsonData.buildError("无可用会话,请重新登录");
|
||||
}
|
||||
String token = tokenEntity.get().getCacheValue();
|
||||
if (token == null || token.isEmpty()) {
|
||||
return JsonData.buildError("无可用会话,请重新登录");
|
||||
}
|
||||
return JsonData.buildSuccess("会话已恢复");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备ID
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ public class LocalJwtAuthInterceptor implements HandlerInterceptor {
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri.startsWith("/libs/") || uri.startsWith("/favicon")
|
||||
|| uri.startsWith("/api/cache") || uri.startsWith("/api/update")) {
|
||||
|| uri.startsWith("/api/update")) {
|
||||
return true;
|
||||
}
|
||||
String auth = request.getHeader("Authorization");
|
||||
|
||||
@@ -1,53 +1,15 @@
|
||||
package com.tashow.erp.test;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import com.tashow.erp.utils.DeviceUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SeleniumWithProfile {
|
||||
private static final Pattern WEIGHT_PATTERN = Pattern.compile("\"(?:unitWeight|weight)\":(\\d+(?:\\.\\d+)?)");
|
||||
public static void main(String[] args) {
|
||||
String uuid = "";
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(
|
||||
new String[]{"wmic", "csproduct", "get", "UUID"});
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) {
|
||||
uuid = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("电脑序列号: " + uuid);
|
||||
// 使用新的 DeviceUtils 获取设备ID
|
||||
String deviceId = DeviceUtils.generateDeviceId();
|
||||
System.out.println("设备ID: " + deviceId);
|
||||
|
||||
// ChromeDriver driver = StealthSelenium.createDriver();
|
||||
// try {
|
||||
//
|
||||
// // 访问目标网站
|
||||
// System.out.println("正在访问目标网站...");
|
||||
// driver.get("https://detail.1688.com/offer/600366775654.html");
|
||||
// // driver.navigate().refresh();
|
||||
// String source = driver.getPageSource();
|
||||
// String weight = "";
|
||||
// Matcher weightMatcher = WEIGHT_PATTERN.matcher(source);
|
||||
// if (weightMatcher.find()) {
|
||||
// String weightValue = weightMatcher.group(1);
|
||||
// weight = " Weight: " + (weightValue.contains(".") ? (int) (Float.parseFloat(weightValue) * 1000) + "g" : weightValue + "g");
|
||||
// }
|
||||
// System.out.println(driver.getTitle());
|
||||
// } catch (Exception e) {
|
||||
// System.err.println("运行出错: " + e.getMessage());
|
||||
// e.printStackTrace();
|
||||
// }finally {
|
||||
// driver.quit();
|
||||
//
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
15
erp_client_sb/src/main/java/com/tashow/erp/test/aa.java
Normal file
15
erp_client_sb/src/main/java/com/tashow/erp/test/aa.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.tashow.erp.test;
|
||||
|
||||
import com.tashow.erp.utils.DeviceUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class aa {
|
||||
@GetMapping("/a")
|
||||
public String aa() {
|
||||
DeviceUtils deviceUtils = new DeviceUtils();
|
||||
return deviceUtils.generateDeviceId();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,234 @@
|
||||
package com.tashow.erp.utils;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.UUID;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* 设备工具类
|
||||
* 设备工具类 - 基于 Windows Registry + PowerShell 实现稳定的设备ID获取
|
||||
* 兼容 Windows 10/11
|
||||
*/
|
||||
public class DeviceUtils {
|
||||
|
||||
/**
|
||||
* 生成设备ID
|
||||
* 优先使用硬件UUID,失败则使用随机UUID
|
||||
* 多重降级策略确保始终能获取到固定的设备标识
|
||||
*
|
||||
* @return 固定的设备ID,格式: 类型前缀_MD5哈希值
|
||||
*/
|
||||
public static String generateDeviceId() {
|
||||
String deviceId = null;
|
||||
|
||||
// 策略1: Windows MachineGuid(注册表)
|
||||
deviceId = getMachineGuid();
|
||||
if (deviceId != null) return deviceId;
|
||||
|
||||
// 策略2: 硬件UUID(PowerShell)
|
||||
deviceId = getHardwareUuid();
|
||||
if (deviceId != null) return deviceId;
|
||||
|
||||
// 策略3: 处理器ID(PowerShell)
|
||||
deviceId = getProcessorId();
|
||||
if (deviceId != null) return deviceId;
|
||||
|
||||
// 策略4: 主板序列号(PowerShell)
|
||||
deviceId = getMotherboardSerial();
|
||||
if (deviceId != null) return deviceId;
|
||||
|
||||
// 策略5: MAC地址(Java原生)
|
||||
deviceId = getMacAddress();
|
||||
if (deviceId != null) return deviceId;
|
||||
|
||||
// 策略6: 系统信息组合(固定哈希)
|
||||
return getSystemInfoHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略1: 获取 Windows MachineGuid(最可靠)
|
||||
* 从注册表读取:HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
|
||||
* 兼容 Win10/Win11,不需要管理员权限
|
||||
*/
|
||||
private static String getMachineGuid() {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"wmic", "csproduct", "get", "UUID"});
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String command = "reg query \"HKLM\\SOFTWARE\\Microsoft\\Cryptography\" /v MachineGuid";
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), "GBK"))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.toLowerCase().contains("uuid")) {
|
||||
return line;
|
||||
if (line.contains("MachineGuid") && line.contains("REG_SZ")) {
|
||||
String[] parts = line.trim().split("\\s+");
|
||||
if (parts.length >= 3) {
|
||||
String guid = parts[parts.length - 1];
|
||||
if (isValidHardwareId(guid)) {
|
||||
return "MGUID_" + SecureUtil.md5(guid).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取 MachineGuid 失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略2: 获取硬件UUID(PowerShell)
|
||||
* 读取 BIOS/UEFI 硬件UUID
|
||||
*/
|
||||
private static String getHardwareUuid() {
|
||||
try {
|
||||
String command = "powershell -Command \"Get-CimInstance Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID\"";
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String uuid = reader.readLine();
|
||||
if (uuid != null) {
|
||||
uuid = uuid.trim();
|
||||
if (isValidHardwareId(uuid)) {
|
||||
return "HW_" + SecureUtil.md5(uuid).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取硬件 UUID 失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略3: 获取处理器ID(PowerShell)
|
||||
*/
|
||||
private static String getProcessorId() {
|
||||
try {
|
||||
String command = "powershell -Command \"Get-CimInstance Win32_Processor | Select-Object -ExpandProperty ProcessorId\"";
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String processorId = reader.readLine();
|
||||
if (processorId != null) {
|
||||
processorId = processorId.trim();
|
||||
if (isValidHardwareId(processorId)) {
|
||||
return "CPU_" + SecureUtil.md5(processorId).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取处理器 ID 失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略4: 获取主板序列号(PowerShell)
|
||||
*/
|
||||
private static String getMotherboardSerial() {
|
||||
try {
|
||||
String command = "powershell -Command \"Get-CimInstance Win32_BaseBoard | Select-Object -ExpandProperty SerialNumber\"";
|
||||
Process process = Runtime.getRuntime().exec(command);
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
String serial = reader.readLine();
|
||||
if (serial != null) {
|
||||
serial = serial.trim();
|
||||
if (isValidHardwareId(serial)) {
|
||||
return "MB_" + SecureUtil.md5(serial).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取主板序列号失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略5: 获取MAC地址(Java原生,第一个物理网卡)
|
||||
*/
|
||||
private static String getMacAddress() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
||||
while (networks.hasMoreElements()) {
|
||||
NetworkInterface network = networks.nextElement();
|
||||
byte[] mac = network.getHardwareAddress();
|
||||
if (mac != null && mac.length == 6) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? ":" : ""));
|
||||
}
|
||||
String macAddress = sb.toString();
|
||||
if (isValidMacAddress(macAddress)) {
|
||||
return "MAC_" + SecureUtil.md5(macAddress).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 静默处理异常
|
||||
System.err.println("获取 MAC 地址失败: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略6: 获取系统信息组合哈希(最终降级方案)
|
||||
* 基于操作系统、计算机名、用户目录等固定信息
|
||||
*/
|
||||
private static String getSystemInfoHash() {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(System.getProperty("os.name", ""));
|
||||
sb.append(System.getProperty("os.version", ""));
|
||||
sb.append(System.getProperty("user.home", ""));
|
||||
sb.append(System.getenv("COMPUTERNAME"));
|
||||
sb.append(System.getenv("USERNAME"));
|
||||
String combined = sb.toString();
|
||||
return "SYS_" + SecureUtil.md5(combined).substring(0, 16).toUpperCase();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取系统信息失败: " + e.getMessage());
|
||||
// 最终的最终降级:时间戳哈希(不推荐,但保证不返回null)
|
||||
return "FALLBACK_" + SecureUtil.md5(String.valueOf(System.currentTimeMillis())).substring(0, 16).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证硬件ID是否有效
|
||||
*/
|
||||
private static boolean isValidHardwareId(String id) {
|
||||
if (id == null || id.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = id.trim().toLowerCase();
|
||||
// 过滤无效值
|
||||
return !normalized.equals("unknown")
|
||||
&& !normalized.equals("n/a")
|
||||
&& !normalized.equals("to be filled by o.e.m.")
|
||||
&& !normalized.equals("default string")
|
||||
&& !normalized.equals("not available")
|
||||
&& !normalized.equals("none")
|
||||
&& !normalized.equals("0")
|
||||
&& !normalized.contains("ffffffff");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证MAC地址是否有效
|
||||
*/
|
||||
private static boolean isValidMacAddress(String mac) {
|
||||
if (mac == null || mac.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 过滤虚拟网卡和无效MAC
|
||||
return !mac.equals("00:00:00:00:00:00")
|
||||
&& !mac.startsWith("00:05:69") // VMware
|
||||
&& !mac.startsWith("00:0C:29") // VMware
|
||||
&& !mac.startsWith("00:50:56") // VMware
|
||||
&& !mac.startsWith("00:1C:42") // Parallels
|
||||
&& !mac.startsWith("00:15:5D"); // Hyper-V
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user