feat: 两步验证增加自定义刷新时间 (#1441)

This commit is contained in:
ssongliu 2023-06-25 17:52:13 +08:00 committed by GitHub
parent 14f7435f82
commit efa3d06673
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 100 additions and 20 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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"`

View file

@ -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
}

View file

@ -32,6 +32,7 @@ func Init() {
migrations.UpdateCronjobWithSecond,
migrations.UpdateWebsite,
migrations.AddBackupAccountDir,
migrations.AddMfaInterval,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -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
},
}

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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 = () => {

View file

@ -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',

View file

@ -1058,6 +1058,8 @@ const message = {
mfaHelper2: '使用手机应用扫描以下二维码获取 6 位验证码',
mfaHelper3: '输入手机应用上的 6 位数字',
mfaCode: '验证码',
mfaInterval: '刷新时间',
mfaIntervalHelper: '修改刷新时间后请重新扫描或手动添加密钥信息',
sslChangeHelper: 'https 设置修改需要重启服务是否继续',
sslDisable: '禁用',
sslDisableHelper: '禁用 https 服务需要重启面板才能生效是否继续',

View file

@ -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;

View file

@ -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;