Files
SamWaf/middleware/auth_api_check.go
T
2026-08-07 10:38:10 +08:00

195 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"SamWaf/common/zlog"
"SamWaf/enums"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/common/response"
"SamWaf/service/waf_service"
"SamWaf/utils"
"SamWaf/wafhostguard"
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
)
var (
wafTokenInfoService = waf_service.WafTokenInfoServiceApp
wafOtpService = waf_service.WafOtpServiceApp
)
// TokenOnlyAuth 仅允许后台 Token 登录访问,拒绝 OpenAPI Key 方式
// 用于保护「开放平台管理」类接口(Key管理、调用日志、API文档),防止通过 API Key 自行操作
func TokenOnlyAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-API-Key") != "" {
response.AuthFailWithMessage("该接口不允许通过 API Key 访问,请使用后台账号登录", c)
c.Abort()
return
}
// 复用 Token 校验逻辑
Auth()(c)
}
}
// Auth 鉴权中间件
func Auth() gin.HandlerFunc {
innerName := "Auth"
return func(c *gin.Context) {
// 优先检查 OpenAPI Key(X-API-Key)
apiKey := c.GetHeader("X-API-Key")
if apiKey != "" {
_, ok := ValidateOpenApiKey(c, apiKey)
if !ok {
c.Abort()
return
}
c.Next()
return
}
// 获取请求头中 token,实际是一个完整被签名过的 token;a complete, signed token
tokenStr := ""
loginType := c.GetHeader("X-Login-Type")
if c.Request.RequestURI == "/api/v1/ws" {
tokenStr = c.GetHeader("Sec-WebSocket-Protocol")
} else if strings.HasPrefix(c.Request.RequestURI, "/api/v1/waflog/attack/download") {
tokenStr = c.Query("X-Token")
} else {
// 根据登录类型获取不同的token头部
if loginType == "mobile" {
tokenStr = c.GetHeader("X-Mobile-Token")
} else {
tokenStr = c.GetHeader("X-Token")
}
}
if tokenStr == "" {
zlog.Debug("无token")
response.AuthFailWithMessage("鉴权失败", c)
c.Abort()
return
} else {
//检查是否存在
isTokenExist := global.GCACHE_WAFCACHE.IsKeyExist(enums.CACHE_TOKEN + tokenStr)
if !isTokenExist {
response.AuthFailWithMessage("令牌过期", c)
c.Abort()
return
} else {
var tokenInfo model.TokenInfo
if err := global.GCACHE_WAFCACHE.GetAs(enums.CACHE_TOKEN+tokenStr, &tokenInfo); err != nil {
response.AuthFailWithMessage("令牌解析失败", c)
c.Abort()
return
}
// IP检查逻辑
currentIP := utils.GetManageClientIP(c)
ipMatched := false
// 如果启用严格IP绑定,进行严格IP检查
if global.GCONFIG_ENABLE_STRICT_IP_BINDING == 1 {
if tokenInfo.LoginIp == currentIP {
ipMatched = true
} else {
ipMatched = false
}
} else {
ipMatched = true
}
// 指纹检查逻辑
fingerprintMatched := true
if global.GCONFIG_ENABLE_DEVICE_FINGERPRINT == 1 && tokenInfo.DeviceFingerprint != "" {
currentFingerprint := utils.GenerateFingerprint(c.Request)
if tokenInfo.DeviceFingerprint == currentFingerprint {
fingerprintMatched = true
} else {
fingerprintMatched = false
}
}
// 如果指纹不匹配,则拒绝请求
if !fingerprintMatched {
zlog.Error(fmt.Sprintf("设备指纹都不匹配,请求拒绝。原IP:%v 当前IP:%v 原指纹:%v 当前指纹:%v",
tokenInfo.LoginIp, currentIP, tokenInfo.DeviceFingerprint, utils.GenerateFingerprint(c.Request)))
//令牌有效但是指纹不匹配,删除缓存
global.GCACHE_WAFCACHE.Remove(enums.CACHE_TOKEN + tokenStr)
response.AuthFailWithMessage("设备验证失败,需要重新登录", c)
c.Abort()
return
}
// N11 修复
// 开启严格IP绑定即要求令牌与登录时的真实 IP 一致,IP 变化需重新登录。
if !ipMatched {
zlog.Warn(fmt.Sprintf("严格IP绑定不匹配,请求拒绝。原IP:%v 当前IP:%v", tokenInfo.LoginIp, currentIP))
//令牌有效但绑定IP不匹配,删除缓存,强制重新登录
global.GCACHE_WAFCACHE.Remove(enums.CACHE_TOKEN + tokenStr)
response.AuthFailWithMessage("登录环境已变化(IP),需要重新登录", c)
c.Abort()
return
}
// 记录"当前正在使用管理端的IP",供主机防爆破豁免。
// 这是防误封的最后一道保险:只要你还在用管理端,你的出口IP就永远进不了封禁名单,
// 哪怕白名单一个字都没配。放在这里是因为走到这说明令牌、指纹、IP绑定都已校验通过。
wafhostguard.TouchAdminIP(currentIP)
//刷新token时间
if global.GWAF_RELEASE == "false" {
tokenList := global.GCACHE_WAFCACHE.ListAvailableKeysWithPrefix(enums.CACHE_TOKEN)
for _, duration := range tokenList {
remainTime := fmt.Sprintf("%02d时%02d分", int(duration.Hours()), int(duration.Minutes())%60)
zlog.Debug(fmt.Sprintf("%v 当前token有效缓存剩余时间 %v", innerName, remainTime))
}
}
expireTime, err := global.GCACHE_WAFCACHE.GetExpireTime(enums.CACHE_TOKEN + tokenStr)
if err == nil {
remainingTime := time.Until(expireTime) // 计算剩余有效时间
if remainingTime > 0 && remainingTime < 2*time.Minute {
zlog.Debug(fmt.Sprintf("%v 当前token有效缓存剩余时间 %v 小于2分钟进行缓存可用时间延期处理", innerName, expireTime))
global.GCACHE_WAFCACHE.SetWithTTlRenewTime(enums.CACHE_TOKEN+tokenStr, tokenInfo, time.Duration(global.GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES)*time.Minute)
}
}
//强制改密门:需改密的令牌只放行改密/注销/ws,其余一律拦截(默认口令/被重置账号在改密前拿不到其他权限)
if tokenInfo.NeedChangePassword == 1 {
p := c.Request.URL.Path
if p != "/api/v1/account/changemypwd" && p != "/api/v1/logout" && p != "/api/v1/ws" {
response.NeedChangePwdWithMessage("请先修改初始/重置密码后再进行其他操作", c)
c.Abort()
return
}
}
//检测是否强制2Fa绑定
if global.GCONFIG_RECORD_FORCE_BIND_2FA == 1 && c.Request.RequestURI != "/api/v1/ws" && c.Request.RequestURI != "/api/v1/logout" {
otpBean := wafOtpService.GetDetailByUserNameApi(tokenInfo.LoginAccount)
if otpBean.UserName == "" {
//需要强制跳转2fa绑定界面
response.NeedBind2FAWithMessage("系统已开启强制 【双因素认证】 ,请进行绑定", c)
c.Abort()
return
}
}
// 将登录账号写入 context,供下游(变更记录、安全审计)取用
c.Set("loginAccount", tokenInfo.LoginAccount)
c.Set("loginIP", currentIP)
// 写入角色,供 RBAC 鉴权中间件判定(空角色兜底为超级管理员,向后兼容)
c.Set("userRole", enums.NormalizeRole(tokenInfo.Role))
}
}
// 这里执行路由 HandlerFunc
c.Next()
}
}