feat(security): 服务器安全日志模块——上报/查询/统计接口 + 015建表菜单
This commit is contained in:
parent
a85a520af7
commit
1d451499b9
@ -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 通过
|
||||
|
||||
91
api/serversecurity/v1/security.go
Normal file
91
api/serversecurity/v1/security.go
Normal file
@ -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"` // 按类型分布
|
||||
}
|
||||
@ -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())
|
||||
})
|
||||
})
|
||||
// 启动招聘模块定时任务(每日增量抓取 + 早报推送)。
|
||||
|
||||
21
internal/controller/serversecurity/controller.go
Normal file
21
internal/controller/serversecurity/controller.go
Normal file
@ -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
|
||||
}
|
||||
57
internal/controller/serversecurity/manage.go
Normal file
57
internal/controller/serversecurity/manage.go
Normal 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
|
||||
}
|
||||
24
internal/controller/serversecurity/report.go
Normal file
24
internal/controller/serversecurity/report.go
Normal 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
|
||||
}
|
||||
18
internal/dao/server_security_log.go
Normal file
18
internal/dao/server_security_log.go
Normal 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()
|
||||
}
|
||||
18
internal/model/do/server_security_log.go
Normal file
18
internal/model/do/server_security_log.go
Normal 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 // 入库时间
|
||||
}
|
||||
74
internal/model/dto/security.go
Normal file
74
internal/model/dto/security.go
Normal 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
|
||||
}
|
||||
14
internal/model/entity/server_security_log.go
Normal file
14
internal/model/entity/server_security_log.go
Normal 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:"入库时间"`
|
||||
}
|
||||
155
internal/service/serversecurity/security.go
Normal file
155
internal/service/serversecurity/security.go
Normal 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
|
||||
}
|
||||
@ -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}"
|
||||
|
||||
38
manifest/sql/015_server_security.sql
Normal file
38
manifest/sql/015_server_security.sql
Normal 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=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);
|
||||
Loading…
Reference in New Issue
Block a user