mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-11 21:06:08 +08:00
perf: optimize login API logic (#11187)
This commit is contained in:
parent
dc8c50c07a
commit
1d34b4aff7
11 changed files with 150 additions and 28 deletions
2
Makefile
2
Makefile
|
|
@ -31,4 +31,4 @@ build_backend_on_darwin:
|
||||||
|
|
||||||
build_all: build_frontend build_backend_on_linux
|
build_all: build_frontend build_backend_on_linux
|
||||||
|
|
||||||
build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin
|
build_on_local: clean_assets build_frontend build_backend_on_darwin
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
"github.com/1Panel-dev/1Panel/backend/global"
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
"github.com/1Panel-dev/1Panel/backend/utils/captcha"
|
"github.com/1Panel-dev/1Panel/backend/utils/captcha"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,7 +28,9 @@ func (b *BaseApi) Login(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.AuthMethod != "jwt" && !req.IgnoreCaptcha {
|
ip := common.GetRealClientIP(c)
|
||||||
|
needCaptcha := global.IPTracker.NeedCaptcha(ip)
|
||||||
|
if needCaptcha {
|
||||||
if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil {
|
if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
return
|
return
|
||||||
|
|
@ -48,9 +52,11 @@ func (b *BaseApi) Login(c *gin.Context) {
|
||||||
user, err := authService.Login(c, req, string(entrance))
|
user, err := authService.Login(c, req, string(entrance))
|
||||||
go saveLoginLogs(c, err)
|
go saveLoginLogs(c, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
global.IPTracker.SetNeedCaptcha(ip)
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
global.IPTracker.Clear(ip)
|
||||||
helper.SuccessWithData(c, user)
|
helper.SuccessWithData(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,16 +140,21 @@ func (b *BaseApi) CheckIsIntl(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Summary Load System Language
|
// @Summary Load System Setting for login
|
||||||
// @Success 200 {string} language
|
// @Success 200 {object} dto.LoginSetting
|
||||||
// @Router /auth/language [get]
|
// @Router /auth/setting [get]
|
||||||
func (b *BaseApi) GetLanguage(c *gin.Context) {
|
func (b *BaseApi) GetAuthSetting(c *gin.Context) {
|
||||||
settingInfo, err := settingService.GetSettingInfo()
|
settingInfo, err := settingService.GetSettingInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, settingInfo.Language)
|
ip := common.GetRealClientIP(c)
|
||||||
|
needCaptcha := global.IPTracker.NeedCaptcha(ip)
|
||||||
|
helper.SuccessWithData(c, dto.LoginSetting{
|
||||||
|
NeedCaptcha: needCaptcha,
|
||||||
|
Language: settingInfo.Language,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLoginLogs(c *gin.Context, err error) {
|
func saveLoginLogs(c *gin.Context, err error) {
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,12 @@ 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"`
|
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
|
||||||
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
|
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
|
||||||
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MFALogin struct {
|
type MFALogin struct {
|
||||||
|
|
@ -38,3 +37,8 @@ type MFALogin struct {
|
||||||
Code string `json:"code" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
AuthMethod string `json:"authMethod"`
|
AuthMethod string `json:"authMethod"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginSetting struct {
|
||||||
|
NeedCaptcha bool `json:"needCaptcha"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/backend/configs"
|
"github.com/1Panel-dev/1Panel/backend/configs"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/init/auth"
|
||||||
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
|
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
|
||||||
"github.com/1Panel-dev/1Panel/backend/init/session/psession"
|
"github.com/1Panel-dev/1Panel/backend/init/session/psession"
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
|
|
@ -28,6 +29,8 @@ var (
|
||||||
MonitorCronID cron.EntryID
|
MonitorCronID cron.EntryID
|
||||||
OneDriveCronID cron.EntryID
|
OneDriveCronID cron.EntryID
|
||||||
|
|
||||||
|
IPTracker *auth.IPTracker
|
||||||
|
|
||||||
I18n *i18n.Localizer
|
I18n *i18n.Localizer
|
||||||
I18nForCmd *i18n.Localizer
|
I18nForCmd *i18n.Localizer
|
||||||
)
|
)
|
||||||
|
|
|
||||||
99
backend/init/auth/auth.go
Normal file
99
backend/init/auth/auth.go
Normal 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:]
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) {
|
||||||
baseRouter.POST("/login", baseApi.Login)
|
baseRouter.POST("/login", baseApi.Login)
|
||||||
baseRouter.POST("/logout", baseApi.LogOut)
|
baseRouter.POST("/logout", baseApi.LogOut)
|
||||||
baseRouter.GET("/demo", baseApi.CheckIsDemo)
|
baseRouter.GET("/demo", baseApi.CheckIsDemo)
|
||||||
baseRouter.GET("/language", baseApi.GetLanguage)
|
baseRouter.GET("/setting", baseApi.GetAuthSetting)
|
||||||
baseRouter.GET("/intl", baseApi.CheckIsIntl)
|
baseRouter.GET("/intl", baseApi.CheckIsIntl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/1Panel-dev/1Panel/backend/i18n"
|
"github.com/1Panel-dev/1Panel/backend/i18n"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/backend/init/app"
|
"github.com/1Panel-dev/1Panel/backend/init/app"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/init/auth"
|
||||||
"github.com/1Panel-dev/1Panel/backend/init/business"
|
"github.com/1Panel-dev/1Panel/backend/init/business"
|
||||||
"github.com/1Panel-dev/1Panel/backend/init/lang"
|
"github.com/1Panel-dev/1Panel/backend/init/lang"
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ func Start() {
|
||||||
business.Init()
|
business.Init()
|
||||||
|
|
||||||
rootRouter := router.Routers()
|
rootRouter := router.Routers()
|
||||||
|
global.IPTracker = auth.NewIPTracker()
|
||||||
|
|
||||||
tcpItem := "tcp4"
|
tcpItem := "tcp4"
|
||||||
if global.CONF.System.Ipv6 == "enable" {
|
if global.CONF.System.Ipv6 == "enable" {
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import (
|
||||||
var store = base64Captcha.DefaultMemStore
|
var store = base64Captcha.DefaultMemStore
|
||||||
|
|
||||||
func VerifyCode(codeID string, code string) error {
|
func VerifyCode(codeID string, code string) error {
|
||||||
if codeID == "" {
|
|
||||||
return constant.ErrCaptchaCode
|
|
||||||
}
|
|
||||||
vv := store.Get(codeID, true)
|
vv := store.Get(codeID, true)
|
||||||
vv = strings.TrimSpace(vv)
|
vv = strings.TrimSpace(vv)
|
||||||
code = strings.TrimSpace(code)
|
code = strings.TrimSpace(code)
|
||||||
|
|
||||||
|
if codeID == "" || code == "" {
|
||||||
|
return constant.ErrCaptchaCode
|
||||||
|
}
|
||||||
if strings.EqualFold(vv, code) {
|
if strings.EqualFold(vv, code) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -26,4 +25,8 @@ export namespace Login {
|
||||||
export interface ResAuthButtons {
|
export interface ResAuthButtons {
|
||||||
[propName: string]: any;
|
[propName: string]: any;
|
||||||
}
|
}
|
||||||
|
export interface LoginSetting {
|
||||||
|
language: string;
|
||||||
|
needCaptcha: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export const checkIsDemo = () => {
|
||||||
return http.get<boolean>('/auth/demo');
|
return http.get<boolean>('/auth/demo');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = () => {
|
export const getAuthSetting = () => {
|
||||||
return http.get<string>(`/auth/language`);
|
return http.get<Login.LoginSetting>(`/auth/setting`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkIsIntl = () => {
|
export const checkIsIntl = () => {
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue';
|
import { ref, reactive, onMounted, computed, nextTick } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { ElForm } from 'element-plus';
|
import type { ElForm } from 'element-plus';
|
||||||
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getLanguage, checkIsIntl } from '@/api/modules/auth';
|
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getAuthSetting, checkIsIntl } from '@/api/modules/auth';
|
||||||
import { GlobalStore, MenuStore, TabsStore } from '@/store';
|
import { GlobalStore, MenuStore, TabsStore } from '@/store';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
@ -318,7 +318,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',
|
||||||
|
|
@ -400,11 +399,12 @@ const checkIsSystemDemo = async () => {
|
||||||
isDemo.value = res.data;
|
isDemo.value = res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLanguage = async () => {
|
const loadLoginSetting = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getLanguage();
|
const res = await getAuthSetting();
|
||||||
loginForm.language = res.data;
|
loginForm.language = res.data.language;
|
||||||
handleCommand(res.data);
|
globalStore.ignoreCaptcha = !res.data.needCaptcha;
|
||||||
|
handleCommand(loginForm.language);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -426,7 +426,7 @@ onMounted(() => {
|
||||||
globalStore.isOnRestart = false;
|
globalStore.isOnRestart = false;
|
||||||
checkIsSystemIntl();
|
checkIsSystemIntl();
|
||||||
loginVerify();
|
loginVerify();
|
||||||
loadLanguage();
|
loadLoginSetting();
|
||||||
document.title = globalStore.themeConfig.panelName;
|
document.title = globalStore.themeConfig.panelName;
|
||||||
loginForm.agreeLicense = globalStore.agreeLicense;
|
loginForm.agreeLicense = globalStore.agreeLicense;
|
||||||
checkIsSystemDemo();
|
checkIsSystemDemo();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue