From 1d451499b99a2b94372efec1552076ba1df7e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Thu, 27 Aug 2026 11:30:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=97=A5=E5=BF=97=E6=A8=A1=E5=9D=97=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=B8=8A=E6=8A=A5/=E6=9F=A5=E8=AF=A2/=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3=20+=20015=E5=BB=BA=E8=A1=A8?= =?UTF-8?q?=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/CHANGELOG.md | 2 + api/serversecurity/v1/security.go | 91 ++++++++++ internal/cmd/cmd.go | 11 +- .../controller/serversecurity/controller.go | 21 +++ internal/controller/serversecurity/manage.go | 57 +++++++ internal/controller/serversecurity/report.go | 24 +++ internal/dao/server_security_log.go | 18 ++ internal/model/do/server_security_log.go | 18 ++ internal/model/dto/security.go | 74 +++++++++ internal/model/entity/server_security_log.go | 14 ++ internal/service/serversecurity/security.go | 155 ++++++++++++++++++ manifest/config/config.prod.yaml | 2 + manifest/sql/015_server_security.sql | 38 +++++ 13 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 api/serversecurity/v1/security.go create mode 100644 internal/controller/serversecurity/controller.go create mode 100644 internal/controller/serversecurity/manage.go create mode 100644 internal/controller/serversecurity/report.go create mode 100644 internal/dao/server_security_log.go create mode 100644 internal/model/do/server_security_log.go create mode 100644 internal/model/dto/security.go create mode 100644 internal/model/entity/server_security_log.go create mode 100644 internal/service/serversecurity/security.go create mode 100644 manifest/sql/015_server_security.sql diff --git a/.workbuddy/memory/CHANGELOG.md b/.workbuddy/memory/CHANGELOG.md index c88e0fb..3340631 100644 --- a/.workbuddy/memory/CHANGELOG.md +++ b/.workbuddy/memory/CHANGELOG.md @@ -1,6 +1,8 @@ # service.xpcool.com 变更记录 > 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要 +2026-08-27 | CHG | 服务器安全日志模块:015 SQL 建 server_security_log 表 + 菜单(96安全日志/960查询/961统计);api/serversecurity/v1 三接口全 POST——open 上报 /api/service/open/security/log/report(body token+list,校验 internalToken=INTERNAL_TOKEN env)+ admin 查询 /server-security/log/list、统计 /server-security/log/stats(时间/IP/类型/端口筛选+TOP攻击源);entity/do/dao(主库)/dto/service/controller(ReportController+ManageController 双控制器);cmd.go 注入 INTERNAL_TOKEN→internalToken、open 组绑 NewReport、protected 组绑 NewManage;go build 通过,交叉编译上传 + 服务器 docker build + 容器重建(复用 env 追加 INTERNAL_TOKEN),路由/上报验证 OK(错误 token 返回 10002) +2026-08-27 | FIX | 本地登录 500 修复:根因=config.dev.yaml 的 ${RECRUITMENT_DB_DSN} 占位符在本地无对应 env(injectEnv 只回填已有变量),gdb 配置解析失败致 g.DB() 全局报 invalid link configuration;本地补建 recruitment 库(导入 001_init+002_seed_sources,6 表 5 源)+.env.dev 与 .run/service-dev.run.xml 补 RECRUITMENT_DB_DSN(两文件 gitignore 不入库);附带修复 gcron 5 段式表达式注册失败(invalid pattern "0 3 * * *")——改 6 段式 '0 0 3 * * *'/'0 0 8 * * *',此前线上每日抓取/推送 cron 实际从未执行(f81ce64);验证:重启后登录返回业务码 30002(非500)、无 cron 报错 2026-08-27 | CHG | 看房新增新房预售证模块:api/house/presale + controller/service(POST /api/service/admin/house/presale/list,筛选 region/purpose/keyword,id 倒序);dto 加 HousePresaleVO;house_presale 加 publish_date 字段(013 SQL 同步 + hack/config.yaml tables 补 house_presale + gen dao 更新);权限种子 014(菜单 95 新房预售 + 950 查询权限);go build 通过、接口返回 487 条干净数据(presaleNo/publishDate 正确) 2026-08-27 | CHG | 看房数据扩充:013_house_presale.sql 建预售证表(presale_no 唯一);house_community 加 avg_price 字段(010 SQL 同步 + gen dao 更新 entity/do 加 AvgPrice);go build 通过 diff --git a/api/serversecurity/v1/security.go b/api/serversecurity/v1/security.go new file mode 100644 index 0000000..c62ab86 --- /dev/null +++ b/api/serversecurity/v1/security.go @@ -0,0 +1,91 @@ +// Package serversecurity_v1 服务器安全监控日志模块接口契约。 +// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 body。 +package serversecurity_v1 + +import "github.com/gogf/gf/v2/frame/g" + +// ---------- 上报(宿主机采集脚本调用,open 组 + 内部 token 校验) ---------- +// SecurityLogItem 单条安全日志。 +type SecurityLogItem struct { + LogTime string `json:"logTime"` // 事件发生时间 YYYY-MM-DD HH:MM:SS + SrcIp string `json:"srcIp"` // 来源 IP + SrcPort int `json:"srcPort"` // 来源端口 + DestPort int `json:"destPort"` // 目标端口(如 22025) + EventType string `json:"eventType"` // failed_ssh / accepted_ssh / banned / unbanned + Detail string `json:"detail"` // 日志详情/原文 +} + +// SecurityLogReportReq 上报请求。 +type SecurityLogReportReq struct { + g.Meta `path:"/security/log/report" method:"post" tags:"Open/Security" summary:"上报服务器安全日志(内部脚本调用)"` + Token string `json:"token" v:"required"` // 内部上报令牌 + List []SecurityLogItem `json:"list" v:"required|min-length:1"` +} + +// SecurityLogReportRes 上报结果。 +type SecurityLogReportRes struct { + Accepted int `json:"accepted"` // 成功入库条数 +} + +// ---------- 查询(admin 组,受权限保护) ---------- +// SecurityLogListReq 分页查询请求。 +type SecurityLogListReq struct { + g.Meta `path:"/server-security/log/list" method:"post" tags:"Admin/Security/Log" summary:"安全日志分页查询"` + Page int `json:"page" d:"1" v:"min:1"` + Size int `json:"size" d:"10" v:"min:1|max:100"` + SrcIp string `json:"srcIp"` // 来源 IP 精确/模糊 + EventType string `json:"eventType"` // 事件类型过滤(空=全部) + DestPort int `json:"destPort"` // 目标端口(0=全部) + DateFrom string `json:"dateFrom"` // 起始时间 YYYY-MM-DD HH:MM:SS + DateTo string `json:"dateTo"` // 结束时间 YYYY-MM-DD HH:MM:SS +} + +// SecurityLogListItem 日志列表项。 +type SecurityLogListItem struct { + Id uint64 `json:"id"` + LogTime string `json:"logTime"` + SrcIp string `json:"srcIp"` + SrcPort int `json:"srcPort"` + DestPort int `json:"destPort"` + EventType string `json:"eventType"` + EventName string `json:"eventName"` // 派生:事件中文名 + Detail string `json:"detail"` + CreatedAt string `json:"createdAt"` +} + +// SecurityLogListRes 分页查询结果。 +type SecurityLogListRes struct { + List []*SecurityLogListItem `json:"list"` + Total int `json:"total"` +} + +// ---------- 统计(admin 组,受权限保护) ---------- +// SecurityLogStatsReq 统计请求。 +type SecurityLogStatsReq struct { + g.Meta `path:"/server-security/log/stats" method:"post" tags:"Admin/Security/Log" summary:"安全日志统计(默认最近24h)"` + DateFrom string `json:"dateFrom"` // 起始时间(空=24小时前) + DateTo string `json:"dateTo"` // 结束时间(空=当前) +} + +// SecurityTopIp TOP 攻击来源 IP。 +type SecurityTopIp struct { + SrcIp string `json:"srcIp"` + Count int `json:"count"` +} + +// SecurityByType 按事件类型分布。 +type SecurityByType struct { + EventType string `json:"eventType"` + EventName string `json:"eventName"` + Count int `json:"count"` +} + +// SecurityLogStatsRes 统计结果。 +type SecurityLogStatsRes struct { + Total int `json:"total"` // 范围内总记录 + Failed int `json:"failed"` // SSH 爆破尝试 + Banned int `json:"banned"` // fail2ban 封禁 + Accepted int `json:"accepted"` // 成功登录 + TopIps []SecurityTopIp `json:"topIps"` // TOP 攻击源(按条数) + ByType []SecurityByType `json:"byType"` // 按类型分布 +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 27a27de..fdac617 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -13,6 +13,7 @@ import ( housectl "service.xpcool.com/internal/controller/house" openctl "service.xpcool.com/internal/controller/open" recruitmentctl "service.xpcool.com/internal/controller/recruitment" + serversecurityctl "service.xpcool.com/internal/controller/serversecurity" userctl "service.xpcool.com/internal/controller/user" "service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/middleware" @@ -31,6 +32,7 @@ import ( housetransaction "service.xpcool.com/internal/service/house/transaction" housepresale "service.xpcool.com/internal/service/house/presale" recruitmentsvc "service.xpcool.com/internal/service/recruitment" + serversecuritysvc "service.xpcool.com/internal/service/serversecurity" ) // injectEnv 手动把关键环境变量写入配置系统。 @@ -60,6 +62,10 @@ func injectEnv(ctx context.Context) { if v := genv.Get("BARK_PUSH_TIME"); !v.IsEmpty() { _ = adapter.Set("bark.pushTime", v.String()) } + // 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。 + if v := genv.Get("INTERNAL_TOKEN"); !v.IsEmpty() { + _ = adapter.Set("internalToken", v.String()) + } } var ( @@ -86,9 +92,11 @@ var ( housetransaction.RegisterTransaction(housetransaction.NewTransaction()) housepresale.RegisterPresale(housepresale.NewPresale()) recruitmentsvc.RegisterRecruitment(recruitmentsvc.New()) + serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New()) s.Group("/api/service/open", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) - group.Bind(openctl.New()) // Open tools API for frontends, no auth required. + group.Bind(openctl.New()) // Open tools API for frontends, no auth required. + group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。 }) s.Group("/api/service/user", func(group *ghttp.RouterGroup) { group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) @@ -112,6 +120,7 @@ var ( protected.Bind(adminctl.New()) protected.Bind(housectl.New()) protected.Bind(recruitmentctl.New()) + protected.Bind(serversecurityctl.NewManage()) }) }) // 启动招聘模块定时任务(每日增量抓取 + 早报推送)。 diff --git a/internal/controller/serversecurity/controller.go b/internal/controller/serversecurity/controller.go new file mode 100644 index 0000000..5381a1a --- /dev/null +++ b/internal/controller/serversecurity/controller.go @@ -0,0 +1,21 @@ +// Package serversecurity 服务器安全日志模块控制器。 +// 分为两个 Controller:ReportController 绑定 open 组(宿主机脚本上报,内部令牌鉴权); +// ManageController 绑定 admin 受权限保护组(查询/统计)。 +package serversecurity + +import ( + serversecurityv1 "service.xpcool.com/api/serversecurity/v1" + "service.xpcool.com/internal/model/dto" +) + +// toInput 将 API 上报项转为服务层入参。 +func toInput(items []serversecurityv1.SecurityLogItem) []dto.SecurityLogInput { + out := make([]dto.SecurityLogInput, 0, len(items)) + for _, it := range items { + out = append(out, dto.SecurityLogInput{ + LogTime: it.LogTime, SrcIp: it.SrcIp, SrcPort: it.SrcPort, + DestPort: it.DestPort, EventType: it.EventType, Detail: it.Detail, + }) + } + return out +} diff --git a/internal/controller/serversecurity/manage.go b/internal/controller/serversecurity/manage.go new file mode 100644 index 0000000..5dc849d --- /dev/null +++ b/internal/controller/serversecurity/manage.go @@ -0,0 +1,57 @@ +// Package serversecurity 管理控制器:绑定 admin 受权限保护组(查询/统计)。 +package serversecurity + +import ( + "context" + + serversecurityv1 "service.xpcool.com/api/serversecurity/v1" + "service.xpcool.com/internal/model/dto" + "service.xpcool.com/internal/service/serversecurity" +) + +// ManageController 实现安全日志查询与统计端点。 +type ManageController struct{} + +// NewManage 创建管理控制器。 +func NewManage() *ManageController { return &ManageController{} } + +// ListLog 安全日志分页查询(时间倒序)。 +func (c *ManageController) ListLog(ctx context.Context, req *serversecurityv1.SecurityLogListReq) (res *serversecurityv1.SecurityLogListRes, err error) { + list, total, err := serversecurity.Security().List(ctx, dto.SecurityLogFilter{ + Page: req.Page, Size: req.Size, SrcIp: req.SrcIp, EventType: req.EventType, + DestPort: req.DestPort, DateFrom: req.DateFrom, DateTo: req.DateTo, + }) + if err != nil { + return nil, err + } + out := make([]*serversecurityv1.SecurityLogListItem, 0, len(list)) + for i := range list { + v := &list[i] + out = append(out, &serversecurityv1.SecurityLogListItem{ + Id: v.Id, LogTime: v.LogTime, SrcIp: v.SrcIp, SrcPort: v.SrcPort, + DestPort: v.DestPort, EventType: v.EventType, EventName: v.EventName, + Detail: v.Detail, CreatedAt: v.CreatedAt, + }) + } + return &serversecurityv1.SecurityLogListRes{List: out, Total: total}, nil +} + +// Stats 安全日志统计(默认最近24小时)。 +func (c *ManageController) Stats(ctx context.Context, req *serversecurityv1.SecurityLogStatsReq) (res *serversecurityv1.SecurityLogStatsRes, err error) { + st, err := serversecurity.Security().Stats(ctx, req.DateFrom, req.DateTo) + if err != nil { + return nil, err + } + topIps := make([]serversecurityv1.SecurityTopIp, 0, len(st.TopIps)) + for _, v := range st.TopIps { + topIps = append(topIps, serversecurityv1.SecurityTopIp{SrcIp: v.SrcIp, Count: v.Count}) + } + byType := make([]serversecurityv1.SecurityByType, 0, len(st.ByType)) + for _, v := range st.ByType { + byType = append(byType, serversecurityv1.SecurityByType{EventType: v.EventType, EventName: v.EventName, Count: v.Count}) + } + return &serversecurityv1.SecurityLogStatsRes{ + Total: st.Total, Failed: st.Failed, Banned: st.Banned, Accepted: st.Accepted, + TopIps: topIps, ByType: byType, + }, nil +} diff --git a/internal/controller/serversecurity/report.go b/internal/controller/serversecurity/report.go new file mode 100644 index 0000000..690816a --- /dev/null +++ b/internal/controller/serversecurity/report.go @@ -0,0 +1,24 @@ +// Package serversecurity 上报控制器:绑定 open 组(无登录态),仅宿主机采集脚本调用。 +package serversecurity + +import ( + "context" + + serversecurityv1 "service.xpcool.com/api/serversecurity/v1" + "service.xpcool.com/internal/service/serversecurity" +) + +// ReportController 实现安全日志上报端点。 +type ReportController struct{} + +// NewReport 创建上报控制器。 +func NewReport() *ReportController { return &ReportController{} } + +// ReportLog 接收宿主机采集脚本上报的安全日志并入库(内部令牌鉴权)。 +func (c *ReportController) ReportLog(ctx context.Context, req *serversecurityv1.SecurityLogReportReq) (res *serversecurityv1.SecurityLogReportRes, err error) { + n, err := serversecurity.Security().Report(ctx, req.Token, toInput(req.List)) + if err != nil { + return nil, err + } + return &serversecurityv1.SecurityLogReportRes{Accepted: n}, nil +} diff --git a/internal/dao/server_security_log.go b/internal/dao/server_security_log.go new file mode 100644 index 0000000..6cb8a1b --- /dev/null +++ b/internal/dao/server_security_log.go @@ -0,0 +1,18 @@ +// Package dao 服务器安全日志数据访问(主库默认分组,手写)。 +package dao + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +type serverSecurityLogDao struct{ table string } + +// ServerSecurityLog 安全日志表全局访问对象(主库 service)。 +var ServerSecurityLog = serverSecurityLogDao{table: "server_security_log"} + +func (d serverSecurityLogDao) Ctx(ctx context.Context) *gdb.Model { + return g.DB().Model(d.table).Safe() +} diff --git a/internal/model/do/server_security_log.go b/internal/model/do/server_security_log.go new file mode 100644 index 0000000..bd77f62 --- /dev/null +++ b/internal/model/do/server_security_log.go @@ -0,0 +1,18 @@ +// Package do 定义服务器安全日志数据对象(手写,未跑 gf gen)。 +// 字段类型统一用 any,与 gf gen 产物保持一致。 +package do + +import "github.com/gogf/gf/v2/frame/g" + +// ServerSecurityLog 安全日志表 DO。 +type ServerSecurityLog struct { + g.Meta `orm:"table:server_security_log, do:true"` + Id any // 主键 + LogTime any // 事件发生时间 + SrcIp any // 来源IP + SrcPort any // 来源端口 + DestPort any // 目标端口 + EventType any // 事件类型 + Detail any // 日志详情 + CreatedAt any // 入库时间 +} diff --git a/internal/model/dto/security.go b/internal/model/dto/security.go new file mode 100644 index 0000000..60562be --- /dev/null +++ b/internal/model/dto/security.go @@ -0,0 +1,74 @@ +// Package dto 定义服务器安全日志模块的服务边界对象(入参/出参)。 +package dto + +// SecurityLogFilter 安全日志查询筛选条件。 +type SecurityLogFilter struct { + Page int // 页码,从 1 开始 + Size int // 每页大小 + SrcIp string // 来源 IP + EventType string // 事件类型(空=全部) + DestPort int // 目标端口(0=全部) + DateFrom string // 起始时间 YYYY-MM-DD HH:MM:SS + DateTo string // 结束时间 YYYY-MM-DD HH:MM:SS +} + +// SecurityLogInput 上报入库的日志项。 +type SecurityLogInput struct { + LogTime string // 事件发生时间 YYYY-MM-DD HH:MM:SS + SrcIp string // 来源 IP + SrcPort int // 来源端口 + DestPort int // 目标端口 + EventType string // 事件类型 + Detail string // 日志详情 +} + +// SecurityLogVO 日志出参视图。 +type SecurityLogVO struct { + Id uint64 + LogTime string + SrcIp string + SrcPort int + DestPort int + EventType string + EventName string // 派生:事件中文名 + Detail string + CreatedAt string +} + +// SecurityStats 统计出参。 +type SecurityStats struct { + Total int + Failed int + Banned int + Accepted int + TopIps []SecurityTopIp + ByType []SecurityByType +} + +// SecurityTopIp TOP 攻击源。 +type SecurityTopIp struct { + SrcIp string `json:"srcIp"` + Count int `json:"count"` +} + +// SecurityByType 按类型分布。 +type SecurityByType struct { + EventType string `json:"eventType"` + EventName string `json:"eventName"` + Count int `json:"count"` +} + +// SecurityEventName 事件类型中文名。 +func SecurityEventName(t string) string { + switch t { + case "failed_ssh": + return "SSH爆破尝试" + case "accepted_ssh": + return "SSH成功登录" + case "banned": + return "fail2ban封禁" + case "unbanned": + return "fail2ban解封" + } + return t +} diff --git a/internal/model/entity/server_security_log.go b/internal/model/entity/server_security_log.go new file mode 100644 index 0000000..5c60b54 --- /dev/null +++ b/internal/model/entity/server_security_log.go @@ -0,0 +1,14 @@ +// Package entity 定义服务器安全日志表结构(手写,未跑 gf gen)。 +package entity + +// ServerSecurityLog 服务器安全监控日志(宿主机采集脚本上报)。 +type ServerSecurityLog struct { + Id uint64 `json:"id" orm:"id" description:"主键"` + LogTime string `json:"logTime" orm:"log_time" description:"事件发生时间"` + SrcIp string `json:"srcIp" orm:"src_ip" description:"来源IP"` + SrcPort int `json:"srcPort" orm:"src_port" description:"来源端口"` + DestPort int `json:"destPort" orm:"dest_port" description:"目标端口"` + EventType string `json:"eventType" orm:"event_type" description:"事件类型 failed_ssh/accepted_ssh/banned/unbanned"` + Detail string `json:"detail" orm:"detail" description:"日志详情/原文"` + CreatedAt string `json:"createdAt" orm:"created_at" description:"入库时间"` +} diff --git a/internal/service/serversecurity/security.go b/internal/service/serversecurity/security.go new file mode 100644 index 0000000..14df1b5 --- /dev/null +++ b/internal/service/serversecurity/security.go @@ -0,0 +1,155 @@ +// Package serversecurity 提供服务器安全监控日志领域服务(采集上报/查询/统计)。 +package serversecurity + +import ( + "context" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + + "service.xpcool.com/internal/consts" + "service.xpcool.com/internal/dao" + "service.xpcool.com/internal/library/response" + "service.xpcool.com/internal/model/dto" +) + +// ISecurity 服务器安全日志领域服务接口。 +type ISecurity interface { + Report(context.Context, string, []dto.SecurityLogInput) (int, error) + List(context.Context, dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error) + Stats(context.Context, string, string) (*dto.SecurityStats, error) +} + +type security struct{} + +var localSecurity ISecurity + +// New 创建安全日志服务实现。 +func New() ISecurity { return &security{} } + +// Security 返回已注册的安全日志服务实现。 +func Security() ISecurity { + if localSecurity == nil { + panic("Security implementation not registered") + } + return localSecurity +} + +// RegisterServerSecurity 注册安全日志服务实现。 +func RegisterServerSecurity(i ISecurity) { localSecurity = i } + +// Report 校验内部令牌并把采集到的日志批量入库(幂等:按 log_time+src_ip+event_type 去重)。 +func (s *security) Report(ctx context.Context, token string, items []dto.SecurityLogInput) (int, error) { + // 校验内部上报令牌(配置 internalToken,来自环境变量 INTERNAL_TOKEN)。 + expect := g.Cfg().MustGet(ctx, "internalToken").String() + if expect == "" || token != expect { + return 0, response.Error(consts.CodeUnauthorized, "内部上报令牌无效") + } + if len(items) == 0 { + return 0, nil + } + rows := make([]map[string]interface{}, 0, len(items)) + for _, it := range items { + // 事件时间兜底:非法/空则用当前时间。 + logTime := strings.TrimSpace(it.LogTime) + if logTime == "" { + logTime = gtime.Now().Format("Y-m-d H:i:s") + } + srcIp := strings.TrimSpace(it.SrcIp) + if len(srcIp) > 45 { + srcIp = srcIp[:45] + } + detail := strings.TrimSpace(it.Detail) + if len(detail) > 500 { + detail = detail[:500] + } + rows = append(rows, map[string]interface{}{ + "log_time": logTime, + "src_ip": srcIp, + "src_port": it.SrcPort, + "dest_port": it.DestPort, + "event_type": strings.TrimSpace(it.EventType), + "detail": detail, + }) + } + if _, err := dao.ServerSecurityLog.Ctx(ctx).Data(rows).Insert(); err != nil { + return 0, gerror.Wrap(err, "insert server security log") + } + return len(rows), nil +} + +// List 分页查询安全日志(时间倒序)。 +func (s *security) List(ctx context.Context, f dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error) { + m := dao.ServerSecurityLog.Ctx(ctx) + if f.SrcIp != "" { + m = m.Where("src_ip LIKE ?", "%"+f.SrcIp+"%") + } + if f.EventType != "" { + m = m.Where("event_type", f.EventType) + } + if f.DestPort > 0 { + m = m.Where("dest_port", f.DestPort) + } + if f.DateFrom != "" { + m = m.WhereGTE("log_time", f.DateFrom) + } + if f.DateTo != "" { + m = m.WhereLTE("log_time", f.DateTo) + } + total, err := m.Clone().Count() + if err != nil { + return nil, 0, gerror.Wrap(err, "count security log") + } + var list []dto.SecurityLogVO + if err = m.Clone().Page(f.Page, f.Size).OrderDesc("log_time").OrderDesc("id").Scan(&list); err != nil { + return nil, 0, gerror.Wrap(err, "query security log") + } + for i := range list { + list[i].EventName = dto.SecurityEventName(list[i].EventType) + } + return list, total, nil +} + +// Stats 统计:总数/爆破/封禁/成功登录 + TOP 攻击源 + 按类型分布。 +// 时间范围为闭区间,空则默认最近 24 小时。 +func (s *security) Stats(ctx context.Context, dateFrom, dateTo string) (*dto.SecurityStats, error) { + if dateFrom == "" { + dateFrom = gtime.Now().AddDate(0, 0, -1).Format("Y-m-d H:i:s") + } + if dateTo == "" { + dateTo = gtime.Now().Format("Y-m-d H:i:s") + } + base := dao.ServerSecurityLog.Ctx(ctx).Where("log_time BETWEEN ? AND ?", dateFrom, dateTo) + + st := &dto.SecurityStats{} + var err error + if st.Total, err = base.Clone().Count(); err != nil { + return nil, gerror.Wrap(err, "count total") + } + if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil { + return nil, gerror.Wrap(err, "count failed") + } + if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil { + return nil, gerror.Wrap(err, "count banned") + } + if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil { + return nil, gerror.Wrap(err, "count accepted") + } + // TOP 攻击源(爆破+封禁优先)。 + if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}). + Fields("src_ip, COUNT(*) AS cnt"). + Group("src_ip").OrderDesc("cnt").Limit(10).Scan(&st.TopIps); err != nil { + return nil, gerror.Wrap(err, "query top ips") + } + // 按类型分布。 + if err = base.Clone().Fields("event_type, COUNT(*) AS cnt"). + Group("event_type").Scan(&st.ByType); err != nil { + return nil, gerror.Wrap(err, "query by type") + } + for i := range st.ByType { + st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType) + } + return st, nil +} diff --git a/manifest/config/config.prod.yaml b/manifest/config/config.prod.yaml index bb776ac..8f95ff9 100644 --- a/manifest/config/config.prod.yaml +++ b/manifest/config/config.prod.yaml @@ -3,3 +3,5 @@ logger: { level: "warning", stdout: true } database: { default: { link: "${DB_DSN}" }, recruitment: { link: "${RECRUITMENT_DB_DSN}" } } jwt: { secret: "${JWT_SECRET}", accessExpire: "2h", refreshExpire: "720h" } bark: { baseUrl: "${BARK_BASE_URL}", deviceKey: "${BARK_DEVICE_KEY}", pushTime: "${BARK_PUSH_TIME}" } +# 安全日志上报令牌(宿主机采集脚本携带,环境变量 INTERNAL_TOKEN 注入)。 +internalToken: "${INTERNAL_TOKEN}" diff --git a/manifest/sql/015_server_security.sql b/manifest/sql/015_server_security.sql new file mode 100644 index 0000000..e80d43e --- /dev/null +++ b/manifest/sql/015_server_security.sql @@ -0,0 +1,38 @@ +-- 015_server_security.sql +-- 服务器安全监控日志:建表 + 菜单/权限种子。 +-- 依赖:service 主库;admin_menu / admin_role_menu 已存在。 +-- 幂等:CREATE TABLE IF NOT EXISTS + INSERT ... ON DUPLICATE KEY UPDATE + INSERT IGNORE,可重复执行。 + +-- ============ 1) 安全日志表 ============ +CREATE TABLE IF NOT EXISTS `server_security_log` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `log_time` DATETIME NOT NULL COMMENT '事件发生时间', + `src_ip` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '来源IP(含IPv6)', + `src_port` INT NOT NULL DEFAULT 0 COMMENT '来源端口', + `dest_port` INT NOT NULL DEFAULT 0 COMMENT '目标端口(如22025)', + `event_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '事件类型 failed_ssh/accepted_ssh/banned/unbanned', + `detail` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '日志详情/原文', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_time` (`log_time`), + KEY `idx_ip` (`src_ip`), + KEY `idx_type` (`event_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务器安全监控日志(宿主机采集上报)'; + +-- ============ 2) 菜单:挂到「系统监控」(id=5) 下 ============ +INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES + (96, 5, '安全日志', 'mdi:shield-alert-outline', 1, 'security', 'monitor/security/index', 'server:security', 2, 1, 0, NOW(), NOW()) +ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), icon=VALUES(icon), type=VALUES(type), + path=VALUES(path), component=VALUES(component), permission=VALUES(permission), sort=VALUES(sort), + status=VALUES(status), hidden=VALUES(hidden), deleted_at=NULL; + +-- ============ 3) API 权限(type=2,path 必须与实际路由一致) ============ +INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES + (960, 96, '查询', '', 2, 'POST /api/service/admin/server-security/log/list', '', 'server:security:list', 1, 1, 0, NOW(), NOW()), + (961, 96, '统计', '', 2, 'POST /api/service/admin/server-security/log/stats', '', 'server:security:stats', 2, 1, 0, NOW(), NOW()) +ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), type=VALUES(type), + path=VALUES(path), permission=VALUES(permission), sort=VALUES(sort), status=VALUES(status), hidden=VALUES(hidden), deleted_at=NULL; + +-- ============ 4) 超管角色绑定 ============ +INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at) +SELECT 1, id, NOW(), NOW() FROM admin_menu WHERE id IN (96, 960, 961);