diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index bdbd20ed..13106869 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -1,6 +1,6 @@ package xiaozhi.modules.config.controller; -import org.apache.commons.lang3.StringUtils; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,13 +9,10 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; -import xiaozhi.common.constant.Constant; -import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.Result; import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.modules.config.dto.AgentModelsDTO; import xiaozhi.modules.config.service.ConfigService; -import xiaozhi.modules.sys.dto.ConfigSecretDTO; import xiaozhi.modules.sys.service.SysParamsService; /** @@ -33,29 +30,17 @@ public class ConfigController { @PostMapping("server-base") @Operation(summary = "获取配置") - public Result getConfig(@RequestBody ConfigSecretDTO dto) { - // 效验数据 - ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); + public Result getConfig() { Object config = configService.getConfig(true); return new Result().ok(config); } @PostMapping("agent-models") @Operation(summary = "获取智能体模型") - public Result getAgentModels(@RequestBody AgentModelsDTO dto) { + public Result getAgentModels(@Valid @RequestBody AgentModelsDTO dto) { // 效验数据 ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule()); return new Result().ok(models); } - - private void checkSecret(String secret) { - String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true); - // 验证密钥 - if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) { - throw new RenException("密钥错误"); - } - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 6e68ce06..62e192b7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -1,9 +1,6 @@ package xiaozhi.modules.security.config; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - +import jakarta.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; @@ -14,10 +11,14 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import jakarta.servlet.Filter; import xiaozhi.modules.security.oauth2.Oauth2Filter; import xiaozhi.modules.security.oauth2.Oauth2Realm; +import xiaozhi.modules.security.secret.ServerSecretFilter; +import xiaozhi.modules.sys.service.SysParamsService; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** * Shiro的配置文件 @@ -46,7 +47,7 @@ public class ShiroConfig { } @Bean("shiroFilter") - public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { + public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) { ShiroFilterConfiguration config = new ShiroFilterConfiguration(); config.setFilterOncePerRequest(true); @@ -54,9 +55,11 @@ public class ShiroConfig { shiroFilter.setSecurityManager(securityManager); shiroFilter.setShiroFilterConfiguration(config); - // oauth过滤 Map filters = new HashMap<>(); + // oauth过滤 filters.put("oauth2", new Oauth2Filter()); + // 服务密钥过滤 + filters.put("server", new ServerSecretFilter(sysParamsService)); shiroFilter.setFilters(filters); // 添加Shiro的内置过滤器 @@ -79,8 +82,8 @@ public class ShiroConfig { filterMap.put("/user/login", "anon"); filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/register", "anon"); - filterMap.put("/config/server-base", "anon"); - filterMap.put("/config/agent-models", "anon"); + // 将config路径使用server服务过滤器 + filterMap.put("/config/**", "server"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); @@ -98,4 +101,4 @@ public class ShiroConfig { advisor.setSecurityManager(securityManager); return advisor; } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java new file mode 100644 index 00000000..b33ce23d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -0,0 +1,105 @@ +package xiaozhi.modules.security.secret; + +import java.io.IOException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.springframework.web.bind.annotation.RequestMethod; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.utils.HttpContextUtils; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.sys.service.SysParamsService; + +/** + * Config API 过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class ServerSecretFilter extends AuthenticatingFilter { + private final SysParamsService sysParamsService; + + @Override + protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) { + // 获取请求token + String token = getRequestToken((HttpServletRequest) request); + + if (StringUtils.isBlank(token)) { + log.warn("createToken:token is empty"); + return null; + } + + return new ServerSecretToken(token); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + // 对OPTIONS请求放行 + if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { + return true; + } + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { + // 获取token并校验 + String token = getRequestToken((HttpServletRequest) servletRequest); + if (StringUtils.isBlank(token)) { + // token为空,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "Authorization token不能为空"); + return false; + } + + // 验证token是否匹配 + String serverSecret = getServerSecret(); + if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { + // token无效,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的Authorization token"); + return false; + } + + return true; + } + + /** + * 发送未授权响应 + */ + private void sendUnauthorizedResponse(HttpServletResponse response, String message) { + response.setContentType("application/json;charset=utf-8"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); + + try { + String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); + response.getWriter().print(json); + } catch (IOException e) { + log.error("响应输出失败", e); + } + } + + /** + * 获取请求的token + */ + private String getRequestToken(HttpServletRequest httpRequest) { + String token = null; + // 从header中获取token + String authorization = httpRequest.getHeader("Authorization"); + if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) { + token = authorization.replace("Bearer ", ""); + } + return token; + } + + private String getServerSecret() { + return sysParamsService.getValue(Constant.SERVER_SECRET, true); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java new file mode 100644 index 00000000..9072eaf8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.security.secret; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Config API Token + */ +public class ServerSecretToken implements AuthenticationToken { + private static final long serialVersionUID = 1L; + private final String token; + + public ServerSecretToken(String token) { + this.token = token; + } + + @Override + public Object getPrincipal() { + return token; + } + + @Override + public Object getCredentials() { + return token; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java deleted file mode 100644 index 8e4978f9..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package xiaozhi.modules.sys.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -@Schema(description = "配置密钥DTO") -public class ConfigSecretDTO { - @Schema(description = "密钥") - @NotBlank(message = "密钥不能为空") - private String secret; -} \ No newline at end of file diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 1f583857..770cbf56 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -53,6 +53,7 @@ class ManageApiClient: headers={ "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})", "Accept": "application/json", + "Authorization": "Bearer " + cls._secret }, timeout=cls.config.get("timeout", 30), # 默认超时时间30秒 ) @@ -126,7 +127,7 @@ class ManageApiClient: def get_server_config() -> Optional[Dict]: """获取服务器基础配置""" return ManageApiClient._instance._execute_request( - "POST", "/config/server-base", json={"secret": ManageApiClient._secret} + "POST", "/config/server-base" ) @@ -138,7 +139,6 @@ def get_agent_models( "POST", "/config/agent-models", json={ - "secret": ManageApiClient._secret, "macAddress": mac_address, "clientId": client_id, "selectedModule": selected_module,