diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 917f1e50..c3f498d4 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -141,6 +141,11 @@ public interface Constant { */ String SERVER_MQTT_SECRET = "server.mqtt_signature_key"; + /** + * WebSocket认证开关 + */ + String SERVER_AUTH_ENABLED = "server.auth.enabled"; + /** * 无记忆 */ diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 5c3f854d..b00af2cf 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -1,6 +1,8 @@ package xiaozhi.modules.device.service.impl; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Base64; import java.util.Date; @@ -169,7 +171,22 @@ public class DeviceServiceImpl extends BaseServiceImpl DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket(); // 从系统参数获取WebSocket URL,如果未配置则使用默认值 String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); - websocket.setToken(""); + + // 检查是否启用认证并生成token + String authEnabled = sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, false); + if ("true".equalsIgnoreCase(authEnabled)) { + try { + // 生成token + String token = generateWebSocketToken(clientId, macAddress); + websocket.setToken(token); + } catch (Exception e) { + log.error("生成WebSocket token失败: {}", e.getMessage()); + websocket.setToken(""); + } + } else { + websocket.setToken(""); + } + if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) { log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置"); wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/"; @@ -494,6 +511,40 @@ public class DeviceServiceImpl extends BaseServiceImpl return Base64.getEncoder().encodeToString(signature); } + /** + * 生成WebSocket认证token 遵循Python端AuthManager的实现逻辑:token = signature.timestamp + * + * @param clientId 客户端ID + * @param username 用户名 (通常为deviceId/macAddress) + * @return 认证token字符串 + */ + private String generateWebSocketToken(String clientId, String username) + throws NoSuchAlgorithmException, InvalidKeyException { + // 从系统参数获取密钥 + String secretKey = sysParamsService.getValue(Constant.SERVER_SECRET, false); + if (StringUtils.isBlank(secretKey)) { + throw new IllegalStateException("WebSocket认证密钥未配置(server.secret)"); + } + + // 获取当前时间戳(秒) + long timestamp = System.currentTimeMillis() / 1000; + + // 构建签名内容: clientId|username|timestamp + String content = String.format("%s|%s|%d", clientId, username, timestamp); + + // 生成HMAC-SHA256签名 + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + hmac.init(keySpec); + byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8)); + + // Base64 URL-safe编码签名(去除填充符=) + String signatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(signature); + + // 返回格式: signature.timestamp + return String.format("%s.%d", signatureBase64, timestamp); + } + /** * 构建MQTT配置信息 * diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index c85510b8..e0220e33 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -68,6 +68,7 @@ async def get_config_from_api_async(config): "url": config["manager-api"].get("url", ""), "secret": config["manager-api"].get("secret", ""), } + auth_enabled = config_data.get("server", {}).get("auth", {}).get("enabled", False) # server的配置以本地为准 if config.get("server"): config_data["server"] = { @@ -77,6 +78,7 @@ async def get_config_from_api_async(config): "vision_explain": config["server"].get("vision_explain", ""), "auth_key": config["server"].get("auth_key", ""), } + config_data["server"]["auth"] = {"enabled": auth_enabled} # 如果服务器没有prompt_template,则从本地配置读取 if not config_data.get("prompt_template"): config_data["prompt_template"] = config.get("prompt_template")