diff --git a/backend/app/api/v1/auth.go b/backend/app/api/v1/auth.go index 9343c55c6..33a34c58f 100644 --- a/backend/app/api/v1/auth.go +++ b/backend/app/api/v1/auth.go @@ -37,6 +37,25 @@ func (b *BaseApi) Login(c *gin.Context) { helper.SuccessWithData(c, user) } +func (b *BaseApi) MFALogin(c *gin.Context) { + var req dto.MFALogin + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + user, err := authService.MFALogin(c, req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, user) +} + func (b *BaseApi) LogOut(c *gin.Context) { if err := authService.LogOut(c); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) diff --git a/backend/app/dto/common_req.go b/backend/app/dto/common_req.go index 583307756..1e1ebdd6e 100644 --- a/backend/app/dto/common_req.go +++ b/backend/app/dto/common_req.go @@ -34,3 +34,11 @@ type Login struct { CaptchaID string `json:"captchaID"` AuthMethod string `json:"authMethod"` } + +type MFALogin struct { + Name string `json:"name" validate:"name,required"` + Password string `json:"password" validate:"required"` + Secret string `json:"secret" validate:"required"` + Code string `json:"code"` + AuthMethod string `json:"authMethod"` +} diff --git a/backend/app/dto/user.go b/backend/app/dto/user.go index e08f5a69b..d5924ea97 100644 --- a/backend/app/dto/user.go +++ b/backend/app/dto/user.go @@ -6,8 +6,10 @@ type CaptchaResponse struct { } type UserLoginInfo struct { - Name string `json:"name"` - Token string `json:"token"` + Name string `json:"name"` + Token string `json:"token"` + MfaStatus string `json:"mfaStatus"` + MfaSecret string `json:"mfaSecret"` } type MfaCredential struct { diff --git a/backend/app/service/auth.go b/backend/app/service/auth.go index ab69ec8e4..67cd7372d 100644 --- a/backend/app/service/auth.go +++ b/backend/app/service/auth.go @@ -39,9 +39,45 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, if err != nil { return nil, err } - if info.Password != pass { + if info.Password != pass && nameSetting.Value == info.Name { return nil, errors.New("login failed") } + mfa, err := settingRepo.Get(settingRepo.WithByKey("MFAStatus")) + if err != nil { + return nil, err + } + if mfa.Value == "enable" { + mfaSecret, err := settingRepo.Get(settingRepo.WithByKey("MFASecret")) + if err != nil { + return nil, err + } + return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value, MfaSecret: mfaSecret.Value}, nil + } + + return u.generateSession(c, info.Name, info.AuthMethod) +} + +func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin) (*dto.UserLoginInfo, error) { + nameSetting, err := settingRepo.Get(settingRepo.WithByKey("UserName")) + if err != nil { + return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error()) + } + passwrodSetting, err := settingRepo.Get(settingRepo.WithByKey("Password")) + if err != nil { + return nil, errors.WithMessage(constant.ErrRecordNotFound, err.Error()) + } + pass, err := encrypt.StringDecrypt(passwrodSetting.Value) + if err != nil { + return nil, err + } + if info.Password != pass && nameSetting.Value == info.Name { + return nil, errors.New("login failed") + } + + return u.generateSession(c, info.Name, info.AuthMethod) +} + +func (u *AuthService) generateSession(c *gin.Context, name, authMethod string) (*dto.UserLoginInfo, error) { setting, err := settingRepo.Get(settingRepo.WithByKey("SessionTimeout")) if err != nil { return nil, err @@ -51,16 +87,16 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, return nil, err } - if info.AuthMethod == constant.AuthMethodJWT { + if authMethod == constant.AuthMethodJWT { j := jwt.NewJWT() claims := j.CreateClaims(jwt.BaseClaims{ - Name: nameSetting.Value, + Name: name, }, lifeTime) token, err := j.CreateToken(claims) if err != nil { return nil, err } - return &dto.UserLoginInfo{Name: nameSetting.Value, Token: token}, err + return &dto.UserLoginInfo{Name: name, Token: token}, nil } sID, _ := c.Cookie(constant.SessionName) sessionUser, err := global.SESSION.Get(sID) @@ -71,13 +107,13 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, if err != nil { return nil, err } - return &dto.UserLoginInfo{Name: nameSetting.Value}, nil + return &dto.UserLoginInfo{Name: name}, nil } if err := global.SESSION.Set(sID, sessionUser, lifeTime); err != nil { return nil, err } - return &dto.UserLoginInfo{Name: nameSetting.Value}, nil + return &dto.UserLoginInfo{Name: name}, nil } func (u *AuthService) LogOut(c *gin.Context) error { diff --git a/backend/router/ro_base.go b/backend/router/ro_base.go index 1f8678d66..e98e138e0 100644 --- a/backend/router/ro_base.go +++ b/backend/router/ro_base.go @@ -14,6 +14,7 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) { baseApi := v1.ApiGroupApp.BaseApi { baseRouter.GET("captcha", baseApi.Captcha) + baseRouter.POST("mfalogin", baseApi.MFALogin) withRecordRouter.POST("login", baseApi.Login) withRecordRouter.POST("logout", baseApi.LogOut) } diff --git a/frontend/src/api/interface/index.ts b/frontend/src/api/interface/index.ts index 91eb55181..1ce916f87 100644 --- a/frontend/src/api/interface/index.ts +++ b/frontend/src/api/interface/index.ts @@ -33,9 +33,18 @@ export namespace Login { captchaID: string; authMethod: string; } + export interface MFALoginForm { + name: string; + password: string; + secret: string; + code: string; + authMethod: string; + } export interface ResLogin { name: string; token: string; + mfaStatus: string; + mfaSecret: string; } export interface ResCaptcha { imagePath: string; diff --git a/frontend/src/api/modules/auth.ts b/frontend/src/api/modules/auth.ts index 0af18b9d0..4e3218d51 100644 --- a/frontend/src/api/modules/auth.ts +++ b/frontend/src/api/modules/auth.ts @@ -5,6 +5,10 @@ export const loginApi = (params: Login.ReqLoginForm) => { return http.post(`/auth/login`, params); }; +export const mfaLoginApi = (params: Login.MFALoginForm) => { + return http.post(`/auth/mfalogin`, params); +}; + export const getCaptcha = () => { return http.get(`/auth/captcha`); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index ed1694e89..fbadcf38e 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -60,6 +60,7 @@ export default { 'Run the following command on the SSH terminal to solve the problem: 1. View the /etc/init.d/bt default command on the panel', warnning: 'Note: [Closing the security entrance] will make your panel login address directly exposed to the Internet, very dangerous, please exercise caution', + codeInput: 'Please enter the 6-digit verification code of the MFA validator', }, rule: { username: 'Please enter a username', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 130561994..ba2ad9788 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -68,6 +68,7 @@ export default { solution: '解决方法:', solutionHelper: '在SSH终端输入以下一种命令来解决 1.查看面板入口:/etc/init.d/bt default', warnning: '注意:【关闭安全入口】将使您的面板登录地址被直接暴露在互联网上,非常危险,请谨慎操作', + codeInput: '请输入MFA验证器的 6 位验证码', }, rule: { username: '请输入用户名', diff --git a/frontend/src/views/login/components/login-form.vue b/frontend/src/views/login/components/login-form.vue index 0eb14b075..222b4f894 100644 --- a/frontend/src/views/login/components/login-form.vue +++ b/frontend/src/views/login/components/login-form.vue @@ -1,42 +1,75 @@ @@ -46,7 +79,7 @@ import { useRouter } from 'vue-router'; import { Login } from '@/api/interface'; import type { ElForm } from 'element-plus'; import { ElMessage } from 'element-plus'; -import { loginApi, getCaptcha } from '@/api/modules/auth'; +import { loginApi, getCaptcha, mfaLoginApi } from '@/api/modules/auth'; import { GlobalStore } from '@/store'; import { MenuStore } from '@/store/modules/menu'; import i18n from '@/lang'; @@ -54,7 +87,6 @@ import i18n from '@/lang'; const globalStore = GlobalStore(); const menuStore = MenuStore(); -// 定义 formRef(校验规则) type FormInstance = InstanceType; const loginFormRef = ref(); const loginRules = reactive({ @@ -62,7 +94,6 @@ const loginRules = reactive({ password: [{ required: true, message: i18n.global.t('commons.rule.password'), trigger: 'blur' }], }); -// 登录表单数据 const loginForm = reactive({ name: 'admin', password: 'Calong@2015', @@ -70,6 +101,13 @@ const loginForm = reactive({ captchaID: '', authMethod: '', }); +const mfaLoginForm = reactive({ + name: 'admin', + password: 'Calong@2015', + secret: '', + code: '', + authMethod: '', +}); const captcha = reactive({ captchaID: '', @@ -78,6 +116,8 @@ const captcha = reactive({ }); const loading = ref(false); +const mfaShow = ref(false); + const router = useRouter(); const login = (formEl: FormInstance | undefined) => { @@ -93,7 +133,12 @@ const login = (formEl: FormInstance | undefined) => { captchaID: captcha.captchaID, authMethod: '', }; - await loginApi(requestLoginForm); + const res = await loginApi(requestLoginForm); + if (res.data.mfaStatus === 'enable') { + mfaShow.value = true; + mfaLoginForm.secret = res.data.mfaSecret; + return; + } globalStore.setLogStatus(true); menuStore.setMenuList([]); ElMessage.success(i18n.global.t('commons.msg.loginSuccess')); @@ -106,6 +151,18 @@ const login = (formEl: FormInstance | undefined) => { }); }; +const mfaLogin = async () => { + if (mfaLoginForm.code) { + mfaLoginForm.name = loginForm.name; + mfaLoginForm.password = loginForm.password; + await mfaLoginApi(mfaLoginForm); + globalStore.setLogStatus(true); + menuStore.setMenuList([]); + ElMessage.success(i18n.global.t('commons.msg.loginSuccess')); + router.push({ name: 'home' }); + } +}; + const resetForm = (formEl: FormInstance | undefined) => { if (!formEl) return; formEl.resetFields(); diff --git a/frontend/src/views/login/index.scss b/frontend/src/views/login/index.scss index 5ec31d8f5..fb301ade8 100644 --- a/frontend/src/views/login/index.scss +++ b/frontend/src/views/login/index.scss @@ -67,5 +67,27 @@ } } } + .login-mfa-form { + width: 420px; + padding: 50px 40px 45px; + border-radius: 10px; + text-align: center; + box-shadow: 2px 3px 7px rgb(0 0 0 / 20%); + .info-text { + margin: 0; + font-size: 24px; + font-weight: 500; + color: #34495e; + white-space: nowrap; + } + .login-btn { + width: 100%; + margin-top: 10px; + white-space: nowrap; + .el-button { + width: 185px; + } + } + } } } diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index 5f0f2ad00..cb3bab638 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -5,13 +5,7 @@ - +
@@ -37,12 +31,8 @@