功能更新
This commit is contained in:
125
internal/middleware/admin.go
Normal file
125
internal/middleware/admin.go
Normal file
@@ -0,0 +1,125 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RequireAPIKey 校验 Authorization: Bearer <api_key>(常数时间比较),不匹配返回 401。
|
||||
func RequireAPIKey(key string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := strings.TrimSpace(strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer "))
|
||||
if key == "" || subtle.ConstantTimeCompare([]byte(token), []byte(key)) != 1 {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
29
internal/middleware/jwt.go
Normal file
29
internal/middleware/jwt.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"nl-pms-api/internal/commonservice"
|
||||
)
|
||||
|
||||
// RequireJWT 校验 Authorization: Bearer <access token>,并把 user_id/username 写入上下文。
|
||||
func RequireJWT(secret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := strings.TrimSpace(strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer "))
|
||||
if token == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"})
|
||||
return
|
||||
}
|
||||
claims, err := commonservice.Parse(secret, token, "access")
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"})
|
||||
return
|
||||
}
|
||||
c.Set(commonservice.CtxUserID, claims.UserID)
|
||||
c.Set(commonservice.CtxUsername, claims.Username)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user