feat: 两步验证支持设置标题,区分系统 (#2411)

Refs #2404
This commit is contained in:
ssongliu 2023-09-28 15:46:18 +08:00 committed by GitHub
parent e8564f38ab
commit c6111de050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 73 additions and 38 deletions

View file

@ -2,10 +2,8 @@ package v1
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path" "path"
"strconv"
"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"
@ -311,23 +309,19 @@ func (b *BaseApi) SystemClean(c *gin.Context) {
// @Tags System Setting // @Tags System Setting
// @Summary Load mfa info // @Summary Load mfa info
// @Description 获取 mfa 信息 // @Description 获取 mfa 信息
// @Param interval path string true "request" // @Accept json
// @Param request body dto.MfaCredential true "request"
// @Success 200 {object} mfa.Otp // @Success 200 {object} mfa.Otp
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /settings/mfa/:interval [get] // @Router /settings/mfa [post]
func (b *BaseApi) GetMFA(c *gin.Context) { func (b *BaseApi) LoadMFA(c *gin.Context) {
intervalStr, ok := c.Params.Get("interval") var req dto.MfaRequest
if !ok { if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error interval in path")) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
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 return
} }
otp, err := mfa.GetOtp("admin", interval) otp, err := mfa.GetOtp("admin", req.Title, req.Interval)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return

View file

@ -11,6 +11,11 @@ type UserLoginInfo struct {
MfaStatus string `json:"mfaStatus"` MfaStatus string `json:"mfaStatus"`
} }
type MfaRequest struct {
Title string `json:"title"`
Interval int `json:"interval"`
}
type MfaCredential struct { type MfaCredential struct {
Secret string `json:"secret"` Secret string `json:"secret"`
Code string `json:"code"` Code string `json:"code"`

View file

@ -30,7 +30,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
settingRouter.GET("/time/option", baseApi.LoadTimeZone) settingRouter.GET("/time/option", baseApi.LoadTimeZone)
settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
settingRouter.GET("/mfa/:interval", baseApi.GetMFA) settingRouter.POST("/mfa", baseApi.LoadMFA)
settingRouter.POST("/mfa/bind", baseApi.MFABind) settingRouter.POST("/mfa/bind", baseApi.MFABind)
settingRouter.POST("/scan", baseApi.ScanSystem) settingRouter.POST("/scan", baseApi.ScanSystem)
settingRouter.POST("/clean", baseApi.SystemClean) settingRouter.POST("/clean", baseApi.SystemClean)

View file

@ -18,11 +18,11 @@ type Otp struct {
QrImage string `json:"qrImage"` QrImage string `json:"qrImage"`
} }
func GetOtp(username string, interval int) (otp Otp, err error) { func GetOtp(username, title string, interval int) (otp Otp, err error) {
secret := gotp.RandomSecret(secretLength) secret := gotp.RandomSecret(secretLength)
otp.Secret = secret otp.Secret = secret
totp := gotp.NewTOTP(secret, 6, interval, nil) totp := gotp.NewTOTP(secret, 6, interval, nil)
uri := totp.ProvisioningUri(username, "1Panel") uri := totp.ProvisioningUri(username, title)
subImg, err := qrcode.Encode(uri, qrcode.Medium, 256) subImg, err := qrcode.Encode(uri, qrcode.Medium, 256)
dist := make([]byte, 3000) dist := make([]byte, 3000)
base64.StdEncoding.Encode(dist, subImg) base64.StdEncoding.Encode(dist, subImg)

View file

@ -8648,25 +8648,30 @@ const docTemplate = `{
} }
} }
}, },
"/settings/mfa/:interval": { "/settings/mfa": {
"get": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 mfa 信息", "description": "获取 mfa 信息",
"consumes": [
"application/json"
],
"tags": [ "tags": [
"System Setting" "System Setting"
], ],
"summary": "Load mfa info", "summary": "Load mfa info",
"parameters": [ "parameters": [
{ {
"type": "string",
"description": "request", "description": "request",
"name": "interval", "name": "request",
"in": "path", "in": "body",
"required": true "required": true,
"schema": {
"$ref": "#/definitions/dto.MfaCredential"
}
} }
], ],
"responses": { "responses": {

View file

@ -8641,25 +8641,30 @@
} }
} }
}, },
"/settings/mfa/:interval": { "/settings/mfa": {
"get": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "获取 mfa 信息", "description": "获取 mfa 信息",
"consumes": [
"application/json"
],
"tags": [ "tags": [
"System Setting" "System Setting"
], ],
"summary": "Load mfa info", "summary": "Load mfa info",
"parameters": [ "parameters": [
{ {
"type": "string",
"description": "request", "description": "request",
"name": "interval", "name": "request",
"in": "path", "in": "body",
"required": true "required": true,
"schema": {
"$ref": "#/definitions/dto.MfaCredential"
}
} }
], ],
"responses": { "responses": {

View file

@ -9611,15 +9611,18 @@ paths:
formatEN: reset an expired Password formatEN: reset an expired Password
formatZH: 重置过期密码 formatZH: 重置过期密码
paramKeys: [] paramKeys: []
/settings/mfa/:interval: /settings/mfa:
get: post:
consumes:
- application/json
description: 获取 mfa 信息 description: 获取 mfa 信息
parameters: parameters:
- description: request - description: request
in: path in: body
name: interval name: request
required: true required: true
type: string schema:
$ref: '#/definitions/dto.MfaCredential'
responses: responses:
"200": "200":
description: OK description: OK

View file

@ -70,6 +70,10 @@ export namespace Setting {
export interface PortUpdate { export interface PortUpdate {
serverPort: number; serverPort: number;
} }
export interface MFARequest {
title: string;
interval: number;
}
export interface MFAInfo { export interface MFAInfo {
secret: string; secret: string;
qrImage: string; qrImage: string;

View file

@ -57,8 +57,8 @@ export const cleanMonitors = () => {
return http.post(`/settings/monitor/clean`, {}); return http.post(`/settings/monitor/clean`, {});
}; };
export const getMFA = (interval: number) => { export const loadMFA = (param: Setting.MFARequest) => {
return http.get<Setting.MFAInfo>(`/settings/mfa/${interval}`, {}); return http.post<Setting.MFAInfo>(`/settings/mfa`, param);
}; };
export const loadDaemonJsonPath = () => { export const loadDaemonJsonPath = () => {

View file

@ -1125,6 +1125,8 @@ const message = {
mfa: 'MFA', mfa: 'MFA',
secret: 'Secret', secret: 'Secret',
mfaInterval: 'Refresh interval (s)', mfaInterval: 'Refresh interval (s)',
mfaTitleHelper:
'Used to differentiate between different 1Panel hosts. After modification, please rescan or manually add the key information!',
mfaIntervalHelper: 'Please rescan or manually add key information after modifying the refresh time.', mfaIntervalHelper: 'Please rescan or manually add key information after modifying the refresh time.',
mfaAlert: mfaAlert:
'MFA password is generated based on the current time. Please ensure that the server time is synchronized.', 'MFA password is generated based on the current time. Please ensure that the server time is synchronized.',

View file

@ -1117,6 +1117,7 @@ const message = {
mfaHelper3: '輸入手機應用上的 6 位數字', mfaHelper3: '輸入手機應用上的 6 位數字',
mfaCode: '驗證碼', mfaCode: '驗證碼',
mfaInterval: '刷新時間', mfaInterval: '刷新時間',
mfaTitleHelper: '用於區分不同 1Panel 主機修改後請重新掃描或手動添加密鑰信息',
mfaIntervalHelper: '修改刷新時間後請重新掃描或手動添加密鑰信息', mfaIntervalHelper: '修改刷新時間後請重新掃描或手動添加密鑰信息',
sslChangeHelper: 'https 設置修改需要重啟服務是否繼續', sslChangeHelper: 'https 設置修改需要重啟服務是否繼續',
sslDisable: '禁用', sslDisable: '禁用',

View file

@ -1118,6 +1118,7 @@ const message = {
mfaHelper3: '输入手机应用上的 6 位数字', mfaHelper3: '输入手机应用上的 6 位数字',
mfaCode: '验证码', mfaCode: '验证码',
mfaInterval: '刷新时间', mfaInterval: '刷新时间',
mfaTitleHelper: '用于区分不同 1Panel 主机修改后请重新扫描或手动添加密钥信息',
mfaIntervalHelper: '修改刷新时间后请重新扫描或手动添加密钥信息', mfaIntervalHelper: '修改刷新时间后请重新扫描或手动添加密钥信息',
sslChangeHelper: 'https 设置修改需要重启服务是否继续', sslChangeHelper: 'https 设置修改需要重启服务是否继续',
sslDisable: '禁用', sslDisable: '禁用',

View file

@ -52,6 +52,16 @@
</div> </div>
</span> </span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.title')" prop="title">
<el-input v-model="form.title">
<template #append>
<el-button @click="loadMfaCodeBefore(formRef)">
{{ $t('commons.button.save') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('setting.mfaTitleHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.mfaInterval')" prop="interval"> <el-form-item :label="$t('setting.mfaInterval')" prop="interval">
<el-input v-model.number="form.interval"> <el-input v-model.number="form.interval">
<template #append> <template #append>
@ -80,7 +90,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { bindMFA, getMFA } from '@/api/modules/setting'; import { bindMFA, loadMFA } from '@/api/modules/setting';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { Rules, checkNumberRange } from '@/global/form-rules'; import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
@ -96,6 +106,7 @@ const drawerVisiable = ref();
const formRef = ref(); const formRef = ref();
const form = reactive({ const form = reactive({
title: '1Panel',
code: '', code: '',
secret: '', secret: '',
interval: 30, interval: 30,
@ -134,7 +145,11 @@ const loadMfaCodeBefore = async (formEl: FormInstance | undefined) => {
loadMfaCode(); loadMfaCode();
}; };
const loadMfaCode = async () => { const loadMfaCode = async () => {
const res = await getMFA(form.interval); let param = {
title: form.title,
interval: form.interval,
};
const res = await loadMFA(param);
form.secret = res.data.secret; form.secret = res.data.secret;
qrImage.value = res.data.qrImage; qrImage.value = res.data.qrImage;
}; };