feat(security): 服务器安全日志模块——上报/查询/统计接口 + 015建表菜单

This commit is contained in:
夏犀麟 2026-08-27 11:30:00 +08:00
parent a85a520af7
commit 1d451499b9
13 changed files with 524 additions and 1 deletions

View File

@ -1,6 +1,8 @@
# service.xpcool.com 变更记录 # service.xpcool.com 变更记录
> 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要 > 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要
2026-08-27 | CHG | 服务器安全日志模块015 SQL server_security_log + 菜单(96安全日志/960查询/961统计)api/serversecurity/v1 三接口全 POSTopen 上报 /api/service/open/security/log/reportbody token+list校验 internalToken=INTERNAL_TOKEN env+ admin 查询 /server-security/log/list统计 /server-security/log/stats时间/IP/类型/端口筛选+TOP攻击源entity/do/dao(主库)/dto/service/controllerReportController+ManageController 双控制器cmd.go 注入 INTERNAL_TOKENinternalTokenopen 组绑 NewReportprotected 组绑 NewManagego build 通过交叉编译上传 + 服务器 docker build + 容器重建复用 env 追加 INTERNAL_TOKEN路由/上报验证 OK错误 token 返回 10002
2026-08-27 | FIX | 本地登录 500 修复根因=config.dev.yaml ${RECRUITMENT_DB_DSN} 占位符在本地无对应 envinjectEnv 只回填已有变量gdb 配置解析失败致 g.DB() 全局报 invalid link configuration本地补建 recruitment 导入 001_init+002_seed_sources6 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/servicePOST /api/service/admin/house/presale/list筛选 region/purpose/keywordid 倒序dto HousePresaleVOhouse_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 | 看房新增新房预售证模块api/house/presale + controller/servicePOST /api/service/admin/house/presale/list筛选 region/purpose/keywordid 倒序dto HousePresaleVOhouse_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 AvgPricego build 通过 2026-08-27 | CHG | 看房数据扩充013_house_presale.sql 建预售证表presale_no 唯一house_community avg_price 字段010 SQL 同步 + gen dao 更新 entity/do AvgPricego build 通过

View File

@ -0,0 +1,91 @@
// Package serversecurity_v1 服务器安全监控日志模块接口契约。
// 规范2026-08-27全部 POSTURL 不含任何参数(查询/路径参数均禁止);入参一律走 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"` // 按类型分布
}

View File

@ -13,6 +13,7 @@ import (
housectl "service.xpcool.com/internal/controller/house" housectl "service.xpcool.com/internal/controller/house"
openctl "service.xpcool.com/internal/controller/open" openctl "service.xpcool.com/internal/controller/open"
recruitmentctl "service.xpcool.com/internal/controller/recruitment" recruitmentctl "service.xpcool.com/internal/controller/recruitment"
serversecurityctl "service.xpcool.com/internal/controller/serversecurity"
userctl "service.xpcool.com/internal/controller/user" userctl "service.xpcool.com/internal/controller/user"
"service.xpcool.com/internal/library/jwt" "service.xpcool.com/internal/library/jwt"
"service.xpcool.com/internal/middleware" "service.xpcool.com/internal/middleware"
@ -31,6 +32,7 @@ import (
housetransaction "service.xpcool.com/internal/service/house/transaction" housetransaction "service.xpcool.com/internal/service/house/transaction"
housepresale "service.xpcool.com/internal/service/house/presale" housepresale "service.xpcool.com/internal/service/house/presale"
recruitmentsvc "service.xpcool.com/internal/service/recruitment" recruitmentsvc "service.xpcool.com/internal/service/recruitment"
serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
) )
// injectEnv 手动把关键环境变量写入配置系统。 // injectEnv 手动把关键环境变量写入配置系统。
@ -60,6 +62,10 @@ func injectEnv(ctx context.Context) {
if v := genv.Get("BARK_PUSH_TIME"); !v.IsEmpty() { if v := genv.Get("BARK_PUSH_TIME"); !v.IsEmpty() {
_ = adapter.Set("bark.pushTime", v.String()) _ = adapter.Set("bark.pushTime", v.String())
} }
// 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。
if v := genv.Get("INTERNAL_TOKEN"); !v.IsEmpty() {
_ = adapter.Set("internalToken", v.String())
}
} }
var ( var (
@ -86,9 +92,11 @@ var (
housetransaction.RegisterTransaction(housetransaction.NewTransaction()) housetransaction.RegisterTransaction(housetransaction.NewTransaction())
housepresale.RegisterPresale(housepresale.NewPresale()) housepresale.RegisterPresale(housepresale.NewPresale())
recruitmentsvc.RegisterRecruitment(recruitmentsvc.New()) recruitmentsvc.RegisterRecruitment(recruitmentsvc.New())
serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New())
s.Group("/api/service/open", func(group *ghttp.RouterGroup) { s.Group("/api/service/open", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) 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) { s.Group("/api/service/user", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse) group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
@ -112,6 +120,7 @@ var (
protected.Bind(adminctl.New()) protected.Bind(adminctl.New())
protected.Bind(housectl.New()) protected.Bind(housectl.New())
protected.Bind(recruitmentctl.New()) protected.Bind(recruitmentctl.New())
protected.Bind(serversecurityctl.NewManage())
}) })
}) })
// 启动招聘模块定时任务(每日增量抓取 + 早报推送)。 // 启动招聘模块定时任务(每日增量抓取 + 早报推送)。

View File

@ -0,0 +1,21 @@
// Package serversecurity 服务器安全日志模块控制器。
// 分为两个 ControllerReportController 绑定 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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 // 入库时间
}

View File

@ -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
}

View File

@ -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:"入库时间"`
}

View File

@ -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
}

View File

@ -3,3 +3,5 @@ logger: { level: "warning", stdout: true }
database: { default: { link: "${DB_DSN}" }, recruitment: { link: "${RECRUITMENT_DB_DSN}" } } database: { default: { link: "${DB_DSN}" }, recruitment: { link: "${RECRUITMENT_DB_DSN}" } }
jwt: { secret: "${JWT_SECRET}", accessExpire: "2h", refreshExpire: "720h" } jwt: { secret: "${JWT_SECRET}", accessExpire: "2h", refreshExpire: "720h" }
bark: { baseUrl: "${BARK_BASE_URL}", deviceKey: "${BARK_DEVICE_KEY}", pushTime: "${BARK_PUSH_TIME}" } bark: { baseUrl: "${BARK_BASE_URL}", deviceKey: "${BARK_DEVICE_KEY}", pushTime: "${BARK_PUSH_TIME}" }
# 安全日志上报令牌宿主机采集脚本携带环境变量 INTERNAL_TOKEN 注入
internalToken: "${INTERNAL_TOKEN}"

View File

@ -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=2path 必须与实际路由一致 ============
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);