Skip to content

Commit ac43f00

Browse files
perf: optimize login API logic (#11104)
1 parent 8ccf1fc commit ac43f00

File tree

9 files changed

+143
-28
lines changed

9 files changed

+143
-28
lines changed

core/app/api/v2/auth.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v2
22

33
import (
44
"encoding/base64"
5+
"github.com/1Panel-dev/1Panel/core/utils/common"
56
"os"
67
"path"
78

@@ -29,12 +30,15 @@ func (b *BaseApi) Login(c *gin.Context) {
2930
return
3031
}
3132

32-
if !req.IgnoreCaptcha {
33+
ip := common.GetRealClientIP(c)
34+
needCaptcha := global.IPTracker.NeedCaptcha(ip)
35+
if needCaptcha {
3336
if errMsg := captcha.VerifyCode(req.CaptchaID, req.Captcha); errMsg != "" {
3437
helper.BadAuth(c, errMsg, nil)
3538
return
3639
}
3740
}
41+
3842
entranceItem := c.Request.Header.Get("EntranceCode")
3943
var entrance []byte
4044
if len(entranceItem) != 0 {
@@ -50,13 +54,18 @@ func (b *BaseApi) Login(c *gin.Context) {
5054
user, msgKey, err := authService.Login(c, req, string(entrance))
5155
go saveLoginLogs(c, err)
5256
if msgKey == "ErrAuth" || msgKey == "ErrEntrance" {
57+
if msgKey == "ErrAuth" {
58+
global.IPTracker.SetNeedCaptcha(ip)
59+
}
5360
helper.BadAuth(c, msgKey, err)
5461
return
5562
}
5663
if err != nil {
64+
global.IPTracker.SetNeedCaptcha(ip)
5765
helper.InternalServer(c, err)
5866
return
5967
}
68+
global.IPTracker.Clear(ip)
6069
helper.SuccessWithData(c, user)
6170
}
6271

@@ -142,15 +151,18 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
142151
helper.InternalServer(c, err)
143152
return
144153
}
154+
ip := common.GetRealClientIP(c)
155+
needCaptcha := global.IPTracker.NeedCaptcha(ip)
145156
res := &dto.LoginSetting{
146-
IsDemo: global.CONF.Base.IsDemo,
147-
IsIntl: global.CONF.Base.IsIntl,
148-
IsFxplay: global.CONF.Base.IsFxplay,
149-
IsOffLine: global.CONF.Base.IsOffLine,
150-
Language: settingInfo.Language,
151-
MenuTabs: settingInfo.MenuTabs,
152-
PanelName: settingInfo.PanelName,
153-
Theme: settingInfo.Theme,
157+
IsDemo: global.CONF.Base.IsDemo,
158+
IsIntl: global.CONF.Base.IsIntl,
159+
IsFxplay: global.CONF.Base.IsFxplay,
160+
IsOffLine: global.CONF.Base.IsOffLine,
161+
Language: settingInfo.Language,
162+
MenuTabs: settingInfo.MenuTabs,
163+
PanelName: settingInfo.PanelName,
164+
Theme: settingInfo.Theme,
165+
NeedCaptcha: needCaptcha,
154166
}
155167
helper.SuccessWithData(c, res)
156168
}

core/app/dto/auth.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ type MfaCredential struct {
2323
}
2424

2525
type Login struct {
26-
Name string `json:"name" validate:"required"`
27-
Password string `json:"password" validate:"required"`
28-
IgnoreCaptcha bool `json:"ignoreCaptcha"`
29-
Captcha string `json:"captcha"`
30-
CaptchaID string `json:"captchaID"`
31-
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"`
26+
Name string `json:"name" validate:"required"`
27+
Password string `json:"password" validate:"required"`
28+
Captcha string `json:"captcha"`
29+
CaptchaID string `json:"captchaID"`
30+
Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"`
3231
}
3332

3433
type MFALogin struct {

core/app/dto/setting.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,13 @@ type AppstoreConfig struct {
241241
}
242242

243243
type LoginSetting struct {
244-
IsDemo bool `json:"isDemo"`
245-
IsIntl bool `json:"isIntl"`
246-
IsOffLine bool `json:"isOffLine"`
247-
IsFxplay bool `json:"isFxplay"`
248-
Language string `json:"language"`
249-
MenuTabs string `json:"menuTabs"`
250-
PanelName string `json:"panelName"`
251-
Theme string `json:"theme"`
244+
IsDemo bool `json:"isDemo"`
245+
IsIntl bool `json:"isIntl"`
246+
IsOffLine bool `json:"isOffLine"`
247+
IsFxplay bool `json:"isFxplay"`
248+
Language string `json:"language"`
249+
MenuTabs string `json:"menuTabs"`
250+
PanelName string `json:"panelName"`
251+
Theme string `json:"theme"`
252+
NeedCaptcha bool `json:"needCaptcha"`
252253
}

core/app/service/auth.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package service
33
import (
44
"crypto/hmac"
55
"encoding/base64"
6-
"strconv"
7-
86
"github.com/1Panel-dev/1Panel/core/app/dto"
97
"github.com/1Panel-dev/1Panel/core/app/repo"
108
"github.com/1Panel-dev/1Panel/core/buserr"
@@ -13,6 +11,7 @@ import (
1311
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
1412
"github.com/1Panel-dev/1Panel/core/utils/mfa"
1513
"github.com/gin-gonic/gin"
14+
"strconv"
1615
)
1716

1817
type AuthService struct{}

core/global/global.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package global
22

33
import (
4+
"github.com/1Panel-dev/1Panel/core/init/auth"
45
"github.com/1Panel-dev/1Panel/core/init/session/psession"
56
"github.com/go-playground/validator/v10"
67
"github.com/nicksnyder/go-i18n/v2/i18n"
@@ -28,6 +29,8 @@ var (
2829
Cron *cron.Cron
2930

3031
ScriptSyncJobID cron.EntryID
32+
33+
IPTracker *auth.IPTracker
3134
)
3235

3336
type DBOption func(*gorm.DB) *gorm.DB

core/init/auth/ip_tracker.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package auth
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
const (
9+
MaxIPCount = 100
10+
ExpireDuration = 30 * time.Minute
11+
)
12+
13+
type IPRecord struct {
14+
NeedCaptcha bool
15+
LastUpdate time.Time
16+
}
17+
18+
type IPTracker struct {
19+
records map[string]*IPRecord
20+
ipOrder []string
21+
mu sync.RWMutex
22+
}
23+
24+
func NewIPTracker() *IPTracker {
25+
return &IPTracker{
26+
records: make(map[string]*IPRecord),
27+
ipOrder: make([]string, 0),
28+
}
29+
}
30+
31+
func (t *IPTracker) NeedCaptcha(ip string) bool {
32+
t.mu.Lock()
33+
defer t.mu.Unlock()
34+
35+
record, exists := t.records[ip]
36+
if !exists {
37+
return false
38+
}
39+
40+
if time.Since(record.LastUpdate) > ExpireDuration {
41+
t.removeIPUnsafe(ip)
42+
return false
43+
}
44+
45+
return record.NeedCaptcha
46+
}
47+
48+
func (t *IPTracker) SetNeedCaptcha(ip string) {
49+
t.mu.Lock()
50+
defer t.mu.Unlock()
51+
52+
if record, exists := t.records[ip]; exists {
53+
if time.Since(record.LastUpdate) > ExpireDuration {
54+
t.removeIPUnsafe(ip)
55+
} else {
56+
record.NeedCaptcha = true
57+
record.LastUpdate = time.Now()
58+
return
59+
}
60+
}
61+
62+
if len(t.records) >= MaxIPCount {
63+
t.removeOldestUnsafe()
64+
}
65+
66+
t.records[ip] = &IPRecord{
67+
NeedCaptcha: true,
68+
LastUpdate: time.Now(),
69+
}
70+
t.ipOrder = append(t.ipOrder, ip)
71+
}
72+
73+
func (t *IPTracker) Clear(ip string) {
74+
t.mu.Lock()
75+
defer t.mu.Unlock()
76+
77+
t.removeIPUnsafe(ip)
78+
}
79+
80+
func (t *IPTracker) removeIPUnsafe(ip string) {
81+
delete(t.records, ip)
82+
83+
for i, storedIP := range t.ipOrder {
84+
if storedIP == ip {
85+
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
86+
break
87+
}
88+
}
89+
}
90+
91+
func (t *IPTracker) removeOldestUnsafe() {
92+
if len(t.ipOrder) == 0 {
93+
return
94+
}
95+
96+
oldestIP := t.ipOrder[0]
97+
delete(t.records, oldestIP)
98+
t.ipOrder = t.ipOrder[1:]
99+
}

core/server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/tls"
55
"encoding/gob"
66
"fmt"
7+
"github.com/1Panel-dev/1Panel/core/init/auth"
78
"net"
89
"net/http"
910
"os"
@@ -54,6 +55,8 @@ func Start() {
5455
gin.SetMode(gin.ReleaseMode)
5556
}
5657

58+
global.IPTracker = auth.NewIPTracker()
59+
5760
tcpItem := "tcp4"
5861
if global.CONF.Conn.Ipv6 == constant.StatusEnable {
5962
tcpItem = "tcp"

frontend/src/api/interface/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ export namespace Login {
22
export interface ReqLoginForm {
33
name: string;
44
password: string;
5-
ignoreCaptcha: boolean;
65
captcha: string;
76
captchaID: string;
87
authMethod: string;
@@ -36,5 +35,6 @@ export namespace Login {
3635
panelName: string;
3736
theme: string;
3837
isOffLine: boolean;
38+
needCaptcha: boolean;
3939
}
4040
}

frontend/src/views/login/components/login-form.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ const loginFormRef = ref<FormInstance>();
220220
const loginForm = reactive({
221221
name: '',
222222
password: '',
223-
ignoreCaptcha: true,
224223
captcha: '',
225224
captchaID: '',
226225
authMethod: 'session',
@@ -318,7 +317,6 @@ const login = (formEl: FormInstance | undefined) => {
318317
let requestLoginForm = {
319318
name: loginForm.name,
320319
password: encryptPassword(loginForm.password),
321-
ignoreCaptcha: globalStore.ignoreCaptcha,
322320
captcha: loginForm.captcha,
323321
captchaID: captcha.captchaID,
324322
authMethod: 'session',
@@ -418,6 +416,7 @@ const getSetting = async () => {
418416
isFxplay.value = res.data.isFxplay;
419417
globalStore.isFxplay = isFxplay.value;
420418
globalStore.isOffLine = res.data.isOffLine;
419+
globalStore.ignoreCaptcha = !res.data.needCaptcha;
421420
422421
document.title = res.data.panelName;
423422
i18n.warnHtmlMessage = false;

0 commit comments

Comments
 (0)