mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-18 22:22:59 +08:00
feat: 两步验证增加自定义刷新时间 (#1441)
This commit is contained in:
parent
14f7435f82
commit
efa3d06673
@ -2,6 +2,8 @@ package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
@ -260,11 +262,23 @@ func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||
// @Tags System Setting
|
||||
// @Summary Load mfa info
|
||||
// @Description 获取 mfa 信息
|
||||
// @Param interval path string true "request"
|
||||
// @Success 200 {object} mfa.Otp
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/mfa [get]
|
||||
// @Router /settings/mfa/:interval [get]
|
||||
func (b *BaseApi) GetMFA(c *gin.Context) {
|
||||
otp, err := mfa.GetOtp("admin")
|
||||
intervalStr, ok := c.Params.Get("interval")
|
||||
if !ok {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error interval in path"))
|
||||
return
|
||||
}
|
||||
interval, err := strconv.Atoi(intervalStr)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("type conversion failed, err: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
otp, err := mfa.GetOtp("admin", interval)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
@ -288,12 +302,17 @@ func (b *BaseApi) MFABind(c *gin.Context) {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
success := mfa.ValidCode(req.Code, req.Secret)
|
||||
success := mfa.ValidCode(req.Code, req.Interval, req.Secret)
|
||||
if !success {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, errors.New("code is not valid"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := settingService.Update("MFAInterval", req.Interval); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := settingService.Update("MFAStatus", "enable"); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
|
@ -12,8 +12,9 @@ type UserLoginInfo struct {
|
||||
}
|
||||
|
||||
type MfaCredential struct {
|
||||
Secret string `json:"secret"`
|
||||
Code string `json:"code"`
|
||||
Secret string `json:"secret"`
|
||||
Code string `json:"code"`
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
|
@ -28,6 +28,7 @@ type SettingInfo struct {
|
||||
ComplexityVerification string `json:"complexityVerification"`
|
||||
MFAStatus string `json:"mfaStatus"`
|
||||
MFASecret string `json:"mfaSecret"`
|
||||
MFAInterval string `json:"mfaInterval"`
|
||||
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
|
@ -76,7 +76,11 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin) (*dto.UserLogi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
success := mfa.ValidCode(info.Code, mfaSecret.Value)
|
||||
mfaInterval, err := settingRepo.Get(settingRepo.WithByKey("MFAInterval"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
success := mfa.ValidCode(info.Code, mfaInterval.Value, mfaSecret.Value)
|
||||
if !success {
|
||||
return nil, constant.ErrAuth
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ func Init() {
|
||||
migrations.UpdateCronjobWithSecond,
|
||||
migrations.UpdateWebsite,
|
||||
migrations.AddBackupAccountDir,
|
||||
migrations.AddMfaInterval,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -400,3 +400,13 @@ var AddBackupAccountDir = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddMfaInterval = &gormigrate.Migration{
|
||||
ID: "20230625-add-mfa-interval",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&model.Setting{Key: "MFAInterval", Value: "30"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
|
||||
settingRouter.GET("/time/option", baseApi.LoadTimeZone)
|
||||
settingRouter.POST("/time/sync", baseApi.SyncTime)
|
||||
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
|
||||
settingRouter.GET("/mfa", baseApi.GetMFA)
|
||||
settingRouter.GET("/mfa/:interval", baseApi.GetMFA)
|
||||
settingRouter.POST("/mfa/bind", baseApi.MFABind)
|
||||
|
||||
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/xlzd/gotp"
|
||||
)
|
||||
@ -17,10 +18,10 @@ type Otp struct {
|
||||
QrImage string `json:"qrImage"`
|
||||
}
|
||||
|
||||
func GetOtp(username string) (otp Otp, err error) {
|
||||
func GetOtp(username string, interval int) (otp Otp, err error) {
|
||||
secret := gotp.RandomSecret(secretLength)
|
||||
otp.Secret = secret
|
||||
totp := gotp.NewDefaultTOTP(secret)
|
||||
totp := gotp.NewTOTP(secret, 6, interval, nil)
|
||||
uri := totp.ProvisioningUri(username, "1Panel")
|
||||
subImg, err := qrcode.Encode(uri, qrcode.Medium, 256)
|
||||
dist := make([]byte, 3000)
|
||||
@ -31,8 +32,13 @@ func GetOtp(username string) (otp Otp, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ValidCode(code string, secret string) bool {
|
||||
totp := gotp.NewDefaultTOTP(secret)
|
||||
func ValidCode(code, intervalStr, secret string) bool {
|
||||
interval, err := strconv.Atoi(intervalStr)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("type conversion failed, err: %v", err)
|
||||
return false
|
||||
}
|
||||
totp := gotp.NewTOTP(secret, 6, interval, nil)
|
||||
now := time.Now().Unix()
|
||||
strInt64 := strconv.FormatInt(now, 10)
|
||||
id16, _ := strconv.Atoi(strInt64)
|
||||
|
@ -27,6 +27,7 @@ export namespace Setting {
|
||||
complexityVerification: string;
|
||||
mfaStatus: string;
|
||||
mfaSecret: string;
|
||||
mfaInterval: string;
|
||||
|
||||
monitorStatus: string;
|
||||
monitorInterval: number;
|
||||
@ -71,6 +72,7 @@ export namespace Setting {
|
||||
export interface MFABind {
|
||||
secret: string;
|
||||
code: string;
|
||||
interval: string;
|
||||
}
|
||||
export interface SnapshotCreate {
|
||||
from: string;
|
||||
|
@ -46,8 +46,8 @@ export const cleanMonitors = () => {
|
||||
return http.post(`/settings/monitor/clean`, {});
|
||||
};
|
||||
|
||||
export const getMFA = () => {
|
||||
return http.get<Setting.MFAInfo>(`/settings/mfa`, {});
|
||||
export const getMFA = (interval: number) => {
|
||||
return http.get<Setting.MFAInfo>(`/settings/mfa/${interval}`, {});
|
||||
};
|
||||
|
||||
export const loadDaemonJsonPath = () => {
|
||||
|
@ -1050,6 +1050,8 @@ const message = {
|
||||
allowIPEgs:
|
||||
'If multiple ip authorizations exist, newlines need to be displayed. For example, \n172.16.10.111 \n172.16.10.112',
|
||||
mfa: 'MFA',
|
||||
mfaInterval: 'Refresh interval (s)',
|
||||
mfaIntervalHelper: 'Please rescan or manually add key information after modifying the refresh time.',
|
||||
mfaAlert:
|
||||
'MFA password is generated based on the current time. Please ensure that the server time is synchronized.',
|
||||
mfaHelper: 'After this function is enabled, the mobile application verification code will be verified',
|
||||
|
@ -1058,6 +1058,8 @@ const message = {
|
||||
mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码',
|
||||
mfaHelper3: '输入手机应用上的 6 位数字',
|
||||
mfaCode: '验证码',
|
||||
mfaInterval: '刷新时间(秒)',
|
||||
mfaIntervalHelper: '修改刷新时间后,请重新扫描或手动添加密钥信息!',
|
||||
sslChangeHelper: 'https 设置修改需要重启服务,是否继续?',
|
||||
sslDisable: '禁用',
|
||||
sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?',
|
||||
|
@ -194,6 +194,7 @@ const form = reactive({
|
||||
expirationTime: '',
|
||||
complexityVerification: 'disable',
|
||||
mfaStatus: 'disable',
|
||||
mfaInterval: 30,
|
||||
allowIPs: '',
|
||||
bindDomain: '',
|
||||
});
|
||||
@ -213,6 +214,7 @@ const search = async () => {
|
||||
form.expirationTime = res.data.expirationTime;
|
||||
form.complexityVerification = res.data.complexityVerification;
|
||||
form.mfaStatus = res.data.mfaStatus;
|
||||
form.mfaInterval = Number(res.data.mfaInterval);
|
||||
form.allowIPs = res.data.allowIPs.replaceAll(',', '\n');
|
||||
form.bindDomain = res.data.bindDomain;
|
||||
};
|
||||
@ -236,7 +238,7 @@ const onSaveComplexity = async () => {
|
||||
|
||||
const handleMFA = async () => {
|
||||
if (form.mfaStatus === 'enable') {
|
||||
mfaRef.value.acceptParams();
|
||||
mfaRef.value.acceptParams({ interval: form.mfaInterval });
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
@ -17,7 +17,14 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-form :model="form" ref="formRef" @submit.prevent v-loading="loading" label-position="top">
|
||||
<el-form
|
||||
:model="form"
|
||||
ref="formRef"
|
||||
@submit.prevent
|
||||
v-loading="loading"
|
||||
label-position="top"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form-item :label="$t('setting.mfaHelper1')">
|
||||
@ -45,7 +52,15 @@
|
||||
</div>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.mfaCode')" prop="code" :rules="Rules.requiredInput">
|
||||
<el-form-item :label="$t('setting.mfaInterval')" prop="interval">
|
||||
<el-input v-model.number="form.interval">
|
||||
<template #append>
|
||||
<el-button @click="loadMfaCode">{{ $t('commons.button.save') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">{{ $t('setting.mfaIntervalHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('setting.mfaCode')" prop="code">
|
||||
<el-input v-model="form.code"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -65,7 +80,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { bindMFA, getMFA } from '@/api/modules/setting';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { FormInstance } from 'element-plus';
|
||||
@ -81,10 +96,20 @@ const formRef = ref();
|
||||
const form = reactive({
|
||||
code: '',
|
||||
secret: '',
|
||||
interval: 30,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
code: [Rules.requiredInput],
|
||||
mfaInterval: [Rules.number, checkNumberRange(15, 300)],
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
interval: number;
|
||||
}
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const acceptParams = (): void => {
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
form.interval = params.interval;
|
||||
loadMfaCode();
|
||||
drawerVisiable.value = true;
|
||||
};
|
||||
@ -99,7 +124,7 @@ const onCopy = async () => {
|
||||
};
|
||||
|
||||
const loadMfaCode = async () => {
|
||||
const res = await getMFA();
|
||||
const res = await getMFA(form.interval);
|
||||
form.secret = res.data.secret;
|
||||
qrImage.value = res.data.qrImage;
|
||||
};
|
||||
@ -108,8 +133,13 @@ const onBind = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
let param = {
|
||||
code: form.code,
|
||||
secret: form.secret,
|
||||
interval: form.interval + '',
|
||||
};
|
||||
loading.value = true;
|
||||
await bindMFA(form)
|
||||
await bindMFA(param)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
drawerVisiable.value = false;
|
||||
|
Loading…
Reference in New Issue
Block a user