Commit 23aee4c4 authored by uuo's avatar uuo

fix: 修复多个服务配置问题并增强安全功能

- 修复Zabbix URL配置,移除冗余路径以支持不同部署方式
- 为auth-service添加RoleLevel字段和godotenv依赖,改进用户注册逻辑
- 在security-service中增强Zabbix API错误处理和重试机制
- 为llm-service助手回复添加敏感词过滤和审计日志
- 统一各服务环境变量加载逻辑,使用pydantic-settings替代直接os.getenv
- 在edu-service的分配教师接口中添加管理员权限检查
- 更新security-service Dockerfile以支持代理头部
- 删除auth-service中已编译的二进制文件
parent 6e2b6222
......@@ -147,7 +147,7 @@ services:
- REDIS_PORT=${REDIS_PORT}
- REDIS_DB=${REDIS_DB}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- ZABBIX_URL=${ZABBIX_URL:-http://localhost/zabbix/api_jsonrpc.php}
- ZABBIX_URL=${ZABBIX_URL:-http://localhost}
- ZABBIX_USERNAME=${ZABBIX_USERNAME:-Admin}
- ZABBIX_PASSWORD=${ZABBIX_PASSWORD:-zabbix}
- ZABBIX_SYNC_INTERVAL=${ZABBIX_SYNC_INTERVAL:-3600}
......
......@@ -150,7 +150,7 @@ services:
- REDIS_PORT=${REDIS_PORT}
- REDIS_DB=${REDIS_DB}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- ZABBIX_URL=${ZABBIX_URL:-http://localhost/zabbix/api_jsonrpc.php}
- ZABBIX_URL=${ZABBIX_URL:-http://localhost}
- ZABBIX_USERNAME=${ZABBIX_USERNAME:-Admin}
- ZABBIX_PASSWORD=${ZABBIX_PASSWORD:-zabbix}
- ZABBIX_SYNC_INTERVAL=${ZABBIX_SYNC_INTERVAL:-3600}
......
module auth-service
go 1.24.3
go 1.24.2
require github.com/gin-gonic/gin v1.11.0
......@@ -12,6 +12,7 @@ require (
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
......
......@@ -59,6 +59,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
......
......@@ -64,6 +64,7 @@ func (s *AuthService) Register(req *RegisterRequest) (*model.User, error) {
Email: req.Email,
Password: hashedPwd,
Role: "user",
RoleLevel: 1, // 默认普通用户等级
Edition: "edu",
}
......
......@@ -10,10 +10,12 @@ import (
"fmt"
"log"
"os"
"path/filepath"
_ "auth-service/docs" // docs is generated by Swag CLI
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"gorm.io/driver/postgres"
......@@ -30,6 +32,19 @@ import (
// @name Authorization
func main() {
// 尝试加载根目录 .env 文件 (用于本地开发)
// 优先级:当前目录 .env > 向上两级 .env > 向上三级 .env
// 注意:Docker 环境下通常不包含这些 .env 文件,而是直接通过 environment 注入,
// 所以这里的加载失败不应该阻断程序运行,只作为开发辅助。
// 1. 尝试当前目录
godotenv.Load()
// 2. 尝试项目根目录 (假设在 microservices/auth-service 下运行)
// 根目录在 ../../.env
rootEnvPath := filepath.Join("..", "..", ".env")
godotenv.Load(rootEnvPath)
// 获取环境变量配置
dbHost := os.Getenv("DB_HOST")
dbUser := os.Getenv("DB_USER")
......
......@@ -2,6 +2,7 @@ package utils
import (
"fmt"
"log"
"os"
"time"
......
......@@ -21,6 +21,12 @@ import org.springframework.context.annotation.Bean;
public class EduServiceApplication {
public static void main(String[] args) {
// 尝试加载根目录 .env 文件 (用于本地开发)
// 注意:生产环境 Docker 会直接注入环境变量,这里仅作为本地开发辅助
// Spring Boot 默认不加载 .env,这里使用 System.setProperty 模拟或推荐使用插件
// 为了简单起见,这里不做复杂的 .env 解析,建议本地开发使用 IDE 插件或手动设置环境变量
// 或者使用 java-dotenv 库
SpringApplication.run(EduServiceApplication.class, args);
}
......
......@@ -6,6 +6,8 @@ import com.llmfilter.edu.service.ScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import com.llmfilter.edu.security.UserContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
......@@ -23,9 +25,18 @@ public class ScheduleController {
@PutMapping("/assign-teacher")
@Operation(summary = "分配任课教师", description = "为特定课程分配任课教师")
public ResponseEntity<Map<String, Boolean>> assignTeacher(@RequestBody AssignTeacherPayload payload) {
public ResponseEntity<Map<String, Object>> assignTeacher(@RequestBody AssignTeacherPayload payload) {
// 权限检查:仅管理员可分配教师
String role = UserContextHolder.getContext().getRole();
if (!"administrator".equals(role)) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "Permission denied: Administrator role required");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
scheduleService.assignTeacher(payload);
Map<String, Boolean> result = new HashMap<>();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return ResponseEntity.ok(result);
}
......
......@@ -21,7 +21,7 @@ import java.security.Key;
@Component
public class JwtAuthenticationFilter implements Filter {
@Value("${jwt.secret:your_secret_key_here}")
@Value("${jwt.secret}")
private String jwtSecret;
private Key key;
......
import os
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# 加载环境变量 (尝试向上查找 .env)
load_dotenv(verbose=True) # 默认查找当前目录
if not os.getenv("MONGODB_URL"):
# 如果没找到,尝试向上级目录查找(本地开发场景)
load_dotenv(dotenv_path="../../.env")
load_dotenv(dotenv_path="../../../.env") # 备用:防止层级变动
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
_ROOT_ENV_PATH = Path(__file__).resolve().parents[4] / ".env"
_SERVICE_ENV_PATH = Path(__file__).resolve().parents[2] / ".env"
model_config = SettingsConfigDict(
env_file=(_ROOT_ENV_PATH, _SERVICE_ENV_PATH),
env_file_encoding="utf-8",
case_sensitive=True,
extra="ignore"
)
# 应用配置
APP_NAME: str = "LLM过滤系统"
API_V1_STR: str = "/api/v1"
APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:8000")
APP_BASE_URL: str = "http://localhost:8000"
# 数据库配置
MONGODB_URL: str = os.getenv("MONGODB_URL", "mongodb://localhost:27017")
DB_NAME: str = os.getenv("DB_NAME", "llm_filter_db")
MONGODB_URL: str = "mongodb://localhost:27017"
DB_NAME: str = "llm_filter_db"
# JWT配置
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
SECRET_KEY: str = ""
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Ollama配置
OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://192.168.6.6:11434/")
OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "deepseek-r1:14b")
OLLAMA_BASE_URL: str = "http://192.168.6.6:11434/"
OLLAMA_MODEL: str = "deepseek-r1:14b"
# Dify配置
DIFY_API_URL: str = os.getenv("DIFY_API_URL", "http://192.168.6.6/v1")
DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "")
DIFY_RESPONSE_MODE: str = os.getenv("DIFY_RESPONSE_MODE", "streaming")
DIFY_MESSAGE_ENDPOINT: str = os.getenv("DIFY_MESSAGE_ENDPOINT", "chat-messages")
DIFY_API_URL: str = "http://192.168.6.6/v1"
# 使用别名从环境变量 DIFY_API_KEY_LLM 读取,代码中仍通过 settings.DIFY_API_KEY 访问
DIFY_API_KEY: str = Field("", alias="DIFY_API_KEY_LLM")
DIFY_RESPONSE_MODE: str = "streaming"
DIFY_MESSAGE_ENDPOINT: str = "chat-messages"
# 应用运行模式开关:仅运行教育版或企业版之一
# 允许的值:"edu" / "biz";若未设置则默认使用 "edu"
# 注意:不再提供混合模式(mixed),如需混合请显式设置并在依赖中放行
APP_MODE: str = os.getenv("APP_MODE", "edu")
CORS_ALLOWED_ORIGINS: str = os.getenv("CORS_ALLOWED_ORIGINS", "*")
GITHUB_DEFAULT_REPO: str = os.getenv("GITHUB_DEFAULT_REPO", "")
GITHUB_TOKEN: str = os.getenv("GITHUB_TOKEN", "")
APP_MODE: str = "edu"
CORS_ALLOWED_ORIGINS: str = "*"
GITHUB_DEFAULT_REPO: str = ""
GITHUB_TOKEN: str = ""
# 学期配置
TERM_START_DATE: str = os.getenv("TERM_START_DATE", "2025-09-01") # 默认开学日期
TERM_START_DATE: str = "2025-09-01" # 默认开学日期
# Redis 配置
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
REDIS_DB: int = 0
REDIS_PASSWORD: str = ""
settings = Settings()
......@@ -236,12 +236,30 @@ async def add_message(conversation_id: str, user_id: str, content: str) -> Dict[
# 调用模型生成回复
assistant_response = await generate_response(model_messages)
# 检查助手回复是否包含敏感词
response_check = await sensitive_word_filter.check_text(assistant_response)
if response_check["contains_sensitive_words"]:
# 记录敏感词审计日志
sensitive_record = {
"user_id": user_id,
"conversation_id": ObjectId(conversation_id),
"message_content": assistant_response, # 记录原始违规内容
"source": "assistant", # 标记来源为助手
"sensitive_words_found": response_check["sensitive_words_found"],
"highest_severity": response_check["highest_severity"],
"timestamp": datetime.now()
}
await db.db.sensitive_records.insert_one(sensitive_record)
# 屏蔽回复内容
assistant_response = "(系统拦截)生成的内容包含敏感信息,已屏蔽。"
# 创建助手回复消息
assistant_message = {
"role": "assistant",
"content": assistant_response,
"timestamp": datetime.now(),
"contains_sensitive_words": False,
"contains_sensitive_words": False, # 既然已屏蔽,标记为不包含(或者是 True 但内容已安全化)
"sensitive_words_found": []
}
......
......@@ -21,5 +21,4 @@ USER appuser
EXPOSE 8000
# 启动命令(生产环境,多worker)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--proxy-headers", "--forwarded-allow-ips", "*"]
from pydantic_settings import BaseSettings
import os
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
_ROOT_ENV_PATH = Path(__file__).resolve().parents[4] / ".env"
_SERVICE_ENV_PATH = Path(__file__).resolve().parents[2] / ".env"
model_config = SettingsConfigDict(
env_file=(_ROOT_ENV_PATH, _SERVICE_ENV_PATH),
env_file_encoding="utf-8",
case_sensitive=True,
)
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "Security Service"
# 鉴权配置 (与 Auth Service 保持一致)
JWT_SECRET: str = os.getenv("JWT_SECRET", "")
ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
JWT_SECRET: str = ""
ALGORITHM: str = "HS256"
# Dify 配置
DIFY_API_URL: str = os.getenv("DIFY_API_URL", "http://192.168.6.6/v1")
DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "")
DIFY_RESPONSE_MODE: str = os.getenv("DIFY_RESPONSE_MODE", "streaming")
DIFY_API_URL: str = "http://192.168.6.6/v1"
# 使用别名从环境变量 DIFY_API_KEY_SECURITY 读取
DIFY_API_KEY: str = Field("", alias="DIFY_API_KEY_SECURITY")
DIFY_RESPONSE_MODE: str = "streaming"
# MongoDB 配置
MONGODB_URL: str = os.getenv("MONGODB_URL", "mongodb://localhost:27017")
MONGODB_DB_NAME: str = os.getenv("MONGODB_DB_NAME", "security_service_db")
MONGODB_URL: str = "mongodb://localhost:27017"
MONGODB_DB_NAME: str = "security_service_db"
# Redis 配置
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
REDIS_DB: int = 0
REDIS_PASSWORD: str = ""
# Zabbix 配置
ZABBIX_URL: str = os.getenv("ZABBIX_URL", "http://localhost/zabbix/api_jsonrpc.php")
ZABBIX_USERNAME: str = os.getenv("ZABBIX_USERNAME", "Admin")
ZABBIX_PASSWORD: str = os.getenv("ZABBIX_PASSWORD", "zabbix")
ZABBIX_URL: str = "http://localhost"
ZABBIX_USERNAME: str = "Admin"
ZABBIX_PASSWORD: str = "zabbix"
# 数据同步配置
ZABBIX_SYNC_INTERVAL: int = int(os.getenv("ZABBIX_SYNC_INTERVAL", "3600")) # 1小时
ZABBIX_AUTO_SYNC: bool = os.getenv("ZABBIX_AUTO_SYNC", "true").lower() == "true"
class Config:
case_sensitive = True
ZABBIX_SYNC_INTERVAL: int = 3600
ZABBIX_AUTO_SYNC: bool = True
settings = Settings()
......@@ -24,6 +24,10 @@ class ZabbixDataCollector:
def _get_api_url(self):
"""返回完整的 API 地址"""
if self.zabbix_url.endswith("api_jsonrpc.php"):
return self.zabbix_url
if self.zabbix_url.endswith("/zabbix"):
return f"{self.zabbix_url}/api_jsonrpc.php"
return f"{self.zabbix_url}/zabbix/api_jsonrpc.php"
def login(self):
......@@ -87,6 +91,10 @@ class ZabbixDataCollector:
def _call_api(self, method: str, params: dict):
"""通用 API 调用"""
return self._call_api_with_retry(method, params, retry_count=1)
def _call_api_with_retry(self, method: str, params: dict, retry_count: int = 0):
"""带重试机制的 API 调用"""
payload = {
"jsonrpc": "2.0",
"method": method,
......@@ -105,7 +113,19 @@ class ZabbixDataCollector:
data = response.json()
if "error" in data:
err = data["error"]
raise Exception(f"[{err.get('code')}] {err.get('message')} - {err.get('data', '')}")
error_msg = err.get('message', '')
error_data = err.get('data', '')
# 检查是否是认证相关错误
# Zabbix API 错误码: -32602 (Invalid params) 有时也用于 session 失效
# 常见 Session 错误信息: "Session terminated", "Not authorized"
if retry_count > 0 and ("Session" in error_data or "authorized" in error_data or "auth" in error_msg.lower()):
logger.warning(f"Zabbix API 认证失败 ({error_msg} - {error_data}),尝试重新登录...")
self.login()
# 更新 auth token 后重试
return self._call_api_with_retry(method, params, retry_count=retry_count - 1)
raise Exception(f"[{err.get('code')}] {error_msg} - {error_data}")
return data["result"]
except Exception as e:
raise Exception(f"API 调用失败 ({method}): {e}")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment