perf: optimize login API logic (#11104)

This commit is contained in:
CityFun 2025-11-28 11:05:27 +08:00 committed by GitHub
parent 8ccf1fcbf3
commit ac43f00273
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 143 additions and 28 deletions

View file

@ -2,6 +2,7 @@ package v2
import ( import (
"encoding/base64" "encoding/base64"
"github.com/1Panel-dev/1Panel/core/utils/common"
"os" "os"
"path" "path"
@ -29,12 +30,15 @@ func (b *BaseApi) Login(c *gin.Context) {
return return
} }
if !req.IgnoreCaptcha { ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
if needCaptcha {
if errMsg := captcha.VerifyCode(req.CaptchaID, req.Captcha); errMsg != "" { if errMsg := captcha.VerifyCode(req.CaptchaID, req.Captcha); errMsg != "" {
helper.BadAuth(c, errMsg, nil) helper.BadAuth(c, errMsg, nil)
return return
} }
} }
entranceItem := c.Request.Header.Get("EntranceCode") entranceItem := c.Request.Header.Get("EntranceCode")
var entrance []byte var entrance []byte
if len(entranceItem) != 0 { if len(entranceItem) != 0 {
@ -50,13 +54,18 @@ func (b *BaseApi) Login(c *gin.Context) {
user, msgKey, err := authService.Login(c, req, string(entrance)) user, msgKey, err := authService.Login(c, req, string(entrance))
go saveLoginLogs(c, err) go saveLoginLogs(c, err)
if msgKey == "ErrAuth" || msgKey == "ErrEntrance" { if msgKey == "ErrAuth" || msgKey == "ErrEntrance" {
if msgKey == "ErrAuth" {
global.IPTracker.SetNeedCaptcha(ip)
}
helper.BadAuth(c, msgKey, err) helper.BadAuth(c, msgKey, err)
return return
} }
if err != nil { if err != nil {
global.IPTracker.SetNeedCaptcha(ip)
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
global.IPTracker.Clear(ip)
helper.SuccessWithData(c, user) helper.SuccessWithData(c, user)
} }
@ -142,6 +151,8 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
res := &dto.LoginSetting{ res := &dto.LoginSetting{
IsDemo: global.CONF.Base.IsDemo, IsDemo: global.CONF.Base.IsDemo,
IsIntl: global.CONF.Base.IsIntl, IsIntl: global.CONF.Base.IsIntl,
@ -151,6 +162,7 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
MenuTabs: settingInfo.MenuTabs, MenuTabs: settingInfo.MenuTabs,
PanelName: settingInfo.PanelName, PanelName: settingInfo.PanelName,
Theme: settingInfo.Theme, Theme: settingInfo.Theme,
NeedCaptcha: needCaptcha,
} }
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }

View file

@ -25,7 +25,6 @@ type MfaCredential struct {
type Login struct { type Login struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
IgnoreCaptcha bool `json:"ignoreCaptcha"`
Captcha string `json:"captcha"` Captcha string `json:"captcha"`
CaptchaID string `json:"captchaID"` CaptchaID string `json:"captchaID"`
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"` Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"`

View file

@ -249,4 +249,5 @@ type LoginSetting struct {
MenuTabs string `json:"menuTabs"` MenuTabs string `json:"menuTabs"`
PanelName string `json:"panelName"` PanelName string `json:"panelName"`
Theme string `json:"theme"` Theme string `json:"theme"`
NeedCaptcha bool `json:"needCaptcha"`
} }

View file

@ -3,8 +3,6 @@ package service
import ( import (
"crypto/hmac" "crypto/hmac"
"encoding/base64" "encoding/base64"
"strconv"
"github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/buserr" "github.com/1Panel-dev/1Panel/core/buserr"
@ -13,6 +11,7 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/encrypt" "github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/1Panel-dev/1Panel/core/utils/mfa" "github.com/1Panel-dev/1Panel/core/utils/mfa"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv"
) )
type AuthService struct{} type AuthService struct{}

View file

@ -1,6 +1,7 @@
package global package global
import ( import (
"github.com/1Panel-dev/1Panel/core/init/auth"
"github.com/1Panel-dev/1Panel/core/init/session/psession" "github.com/1Panel-dev/1Panel/core/init/session/psession"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
@ -28,6 +29,8 @@ var (
Cron *cron.Cron Cron *cron.Cron
ScriptSyncJobID cron.EntryID ScriptSyncJobID cron.EntryID
IPTracker *auth.IPTracker
) )
type DBOption func(*gorm.DB) *gorm.DB type DBOption func(*gorm.DB) *gorm.DB

View file

@ -0,0 +1,99 @@
package auth
import (
"sync"
"time"
)
const (
MaxIPCount = 100
ExpireDuration = 30 * time.Minute
)
type IPRecord struct {
NeedCaptcha bool
LastUpdate time.Time
}
type IPTracker struct {
records map[string]*IPRecord
ipOrder []string
mu sync.RWMutex
}
func NewIPTracker() *IPTracker {
return &IPTracker{
records: make(map[string]*IPRecord),
ipOrder: make([]string, 0),
}
}
func (t *IPTracker) NeedCaptcha(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()
record, exists := t.records[ip]
if !exists {
return false
}
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
return false
}
return record.NeedCaptcha
}
func (t *IPTracker) SetNeedCaptcha(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
if record, exists := t.records[ip]; exists {
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
} else {
record.NeedCaptcha = true
record.LastUpdate = time.Now()
return
}
}
if len(t.records) >= MaxIPCount {
t.removeOldestUnsafe()
}
t.records[ip] = &IPRecord{
NeedCaptcha: true,
LastUpdate: time.Now(),
}
t.ipOrder = append(t.ipOrder, ip)
}
func (t *IPTracker) Clear(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
t.removeIPUnsafe(ip)
}
func (t *IPTracker) removeIPUnsafe(ip string) {
delete(t.records, ip)
for i, storedIP := range t.ipOrder {
if storedIP == ip {
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
break
}
}
}
func (t *IPTracker) removeOldestUnsafe() {
if len(t.ipOrder) == 0 {
return
}
oldestIP := t.ipOrder[0]
delete(t.records, oldestIP)
t.ipOrder = t.ipOrder[1:]
}

View file

@ -4,6 +4,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/core/init/auth"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -54,6 +55,8 @@ func Start() {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
global.IPTracker = auth.NewIPTracker()
tcpItem := "tcp4" tcpItem := "tcp4"
if global.CONF.Conn.Ipv6 == constant.StatusEnable { if global.CONF.Conn.Ipv6 == constant.StatusEnable {
tcpItem = "tcp" tcpItem = "tcp"

View file

@ -2,7 +2,6 @@ export namespace Login {
export interface ReqLoginForm { export interface ReqLoginForm {
name: string; name: string;
password: string; password: string;
ignoreCaptcha: boolean;
captcha: string; captcha: string;
captchaID: string; captchaID: string;
authMethod: string; authMethod: string;
@ -36,5 +35,6 @@ export namespace Login {
panelName: string; panelName: string;
theme: string; theme: string;
isOffLine: boolean; isOffLine: boolean;
needCaptcha: boolean;
} }
} }

View file

@ -220,7 +220,6 @@ const loginFormRef = ref<FormInstance>();
const loginForm = reactive({ const loginForm = reactive({
name: '', name: '',
password: '', password: '',
ignoreCaptcha: true,
captcha: '', captcha: '',
captchaID: '', captchaID: '',
authMethod: 'session', authMethod: 'session',
@ -318,7 +317,6 @@ const login = (formEl: FormInstance | undefined) => {
let requestLoginForm = { let requestLoginForm = {
name: loginForm.name, name: loginForm.name,
password: encryptPassword(loginForm.password), password: encryptPassword(loginForm.password),
ignoreCaptcha: globalStore.ignoreCaptcha,
captcha: loginForm.captcha, captcha: loginForm.captcha,
captchaID: captcha.captchaID, captchaID: captcha.captchaID,
authMethod: 'session', authMethod: 'session',
@ -418,6 +416,7 @@ const getSetting = async () => {
isFxplay.value = res.data.isFxplay; isFxplay.value = res.data.isFxplay;
globalStore.isFxplay = isFxplay.value; globalStore.isFxplay = isFxplay.value;
globalStore.isOffLine = res.data.isOffLine; globalStore.isOffLine = res.data.isOffLine;
globalStore.ignoreCaptcha = !res.data.needCaptcha;
document.title = res.data.panelName; document.title = res.data.panelName;
i18n.warnHtmlMessage = false; i18n.warnHtmlMessage = false;