126 lines
3.4 KiB
Go
126 lines
3.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"nl-pms-api/internal/commonservice"
|
|
"nl-pms-api/internal/model"
|
|
)
|
|
|
|
const (
|
|
HeaderAdminStepUp = "X-Admin-StepUp"
|
|
CtxClientIP = "client_ip"
|
|
)
|
|
|
|
// AttachClientIP 把解析后的客户端 IP 写入上下文。
|
|
func AttachClientIP() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Set(CtxClientIP, strings.TrimSpace(c.ClientIP()))
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ClientIP 读取中间件写入的客户端 IP。
|
|
func ClientIP(c *gin.Context) string {
|
|
v, _ := c.Get(CtxClientIP)
|
|
s, _ := v.(string)
|
|
if s == "" {
|
|
return strings.TrimSpace(c.ClientIP())
|
|
}
|
|
return s
|
|
}
|
|
|
|
// RejectDisabled 拒绝已禁用账号(在 JWT 之后)。
|
|
func RejectDisabled(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
uid := commonservice.UserID(c)
|
|
if uid <= 0 {
|
|
c.Next()
|
|
return
|
|
}
|
|
var u model.User
|
|
if err := db.Select("id", "disabled").First(&u, uid).Error; err != nil {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"})
|
|
return
|
|
}
|
|
if u.Disabled != 0 {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ACCOUNT_DISABLED"})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireAdmin 仅允许 users.id=1。
|
|
func RequireAdmin() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if commonservice.UserID(c) != commonservice.AdminUserID {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "FORBIDDEN"})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireAdminStepUp 校验 2 小时内有效的动态码二次验证,且 IP 与签发时一致。
|
|
func RequireAdminStepUp(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if commonservice.UserID(c) != commonservice.AdminUserID {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "FORBIDDEN"})
|
|
return
|
|
}
|
|
token := strings.TrimSpace(c.GetHeader(HeaderAdminStepUp))
|
|
if token == "" {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"})
|
|
return
|
|
}
|
|
ip := ClientIP(c)
|
|
var row model.AdminStepup
|
|
if err := db.First(&row, commonservice.AdminUserID).Error; err != nil {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"})
|
|
return
|
|
}
|
|
sum := sha256.Sum256([]byte(token))
|
|
hash := hex.EncodeToString(sum[:])
|
|
if row.TokenHash != hash {
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"})
|
|
return
|
|
}
|
|
exp, err := time.Parse(time.RFC3339, row.ExpiresAt)
|
|
if err != nil || time.Now().UTC().After(exp) {
|
|
_ = db.Delete(&model.AdminStepup{}, commonservice.AdminUserID).Error
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"})
|
|
return
|
|
}
|
|
if row.ClientIP != "" && ip != "" && row.ClientIP != ip {
|
|
_ = db.Delete(&model.AdminStepup{}, commonservice.AdminUserID).Error
|
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
|
"error": "ADMIN_IP_CHANGED",
|
|
"message": "检测到 IP 变化,存在账号被盗风险,请重新输入动态码",
|
|
})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireStepUpForGlobalSetting 仅当写入全局设置键时要求 stepup。
|
|
func RequireStepUpForGlobalSetting(db *gorm.DB) gin.HandlerFunc {
|
|
inner := RequireAdminStepUp(db)
|
|
return func(c *gin.Context) {
|
|
name := strings.TrimSpace(c.Param("name"))
|
|
if name == "file_storage" || strings.HasPrefix(name, "fest_img:") {
|
|
inner(c)
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|