Merge pull request #1060 from GOODDAYDAY/feature/config-token

feat: 增加server通用secret过滤器
This commit is contained in:
hrz
2025-04-29 22:45:33 +08:00
committed by GitHub
6 changed files with 149 additions and 44 deletions
@@ -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<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
public Result<Object> getConfig() {
Object config = configService.getConfig(true);
return new Result<Object>().ok(config);
}
@PostMapping("agent-models")
@Operation(summary = "获取智能体模型")
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
return new Result<Object>().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("密钥错误");
}
}
}
@@ -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<String, Filter> 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;
}
}
}
@@ -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<Void>().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);
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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,