feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"simple_portal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FailRecord stores the login failure count and first failure time for an IP.
|
||||
type FailRecord struct {
|
||||
Count int
|
||||
FirstAt time.Time
|
||||
}
|
||||
|
||||
// IPBanGuard maintains an in-memory counter of login failures per IP.
|
||||
// When 5 failures occur within 5 minutes, the IP is automatically banned.
|
||||
type IPBanGuard struct {
|
||||
failMap sync.Map // map[string]*FailRecord
|
||||
}
|
||||
|
||||
// NewIPBanGuard creates a new IPBanGuard instance.
|
||||
func NewIPBanGuard() *IPBanGuard {
|
||||
return &IPBanGuard{}
|
||||
}
|
||||
|
||||
// RecordFail records a login failure for the given IP.
|
||||
// It returns true if the IP should be auto-banned (5 failures within 5 minutes).
|
||||
// The caller is responsible for creating the actual ban record in the database.
|
||||
func (g *IPBanGuard) RecordFail(ip string) bool {
|
||||
now := time.Now()
|
||||
|
||||
// Try to load existing record
|
||||
if val, ok := g.failMap.Load(ip); ok {
|
||||
rec := val.(*FailRecord)
|
||||
// Check if within 5-minute window
|
||||
if now.Sub(rec.FirstAt) > 5*time.Minute {
|
||||
// Reset — outside the window
|
||||
rec.Count = 1
|
||||
rec.FirstAt = now
|
||||
return false
|
||||
}
|
||||
rec.Count++
|
||||
if rec.Count >= 5 {
|
||||
// Auto-ban: reset the counter after creating the ban
|
||||
g.failMap.Delete(ip)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// New record
|
||||
g.failMap.Store(ip, &FailRecord{Count: 1, FirstAt: now})
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetFail resets the failure counter for the given IP (on successful login).
|
||||
func (g *IPBanGuard) ResetFail(ip string) {
|
||||
g.failMap.Delete(ip)
|
||||
}
|
||||
|
||||
// IPWhitelistRequired returns a Gin middleware that checks if the visitor's IP
|
||||
// is in the whitelist. Only when the whitelist table has records does it restrict
|
||||
// access; when the whitelist is empty, no IP is restricted.
|
||||
//
|
||||
// 登录页面路由(/admin/login)始终放行。
|
||||
// 已通过 session 认证的用户也放行,避免管理员添加白名单后自己被锁定。
|
||||
// sessionCheck 是一个函数,接收 sessionID 返回是否有效。
|
||||
func IPWhitelistRequired(sessionCheck func(sessionID string) bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 登录页面始终放行
|
||||
path := c.Request.URL.Path
|
||||
if path == "/admin/login" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
hasWhitelist, err := models.HasWhitelist()
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if !hasWhitelist {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
allowed, err := models.IsIPWhitelisted(clientIP)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if allowed {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// IP不在白名单中,但如果有有效session则放行
|
||||
if sessionID, err := c.Cookie("session_id"); err == nil && sessionID != "" {
|
||||
if sessionCheck(sessionID) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusForbidden, "admin/403.html", gin.H{
|
||||
"Title": "访问被拒绝",
|
||||
"IP": clientIP,
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user