Browse Source

feat: 增加系统安全入口功能

pull/774/head
ssongliu 2 years ago committed by ssongliu
parent
commit
d5f400670c
  1. 39
      backend/app/api/v1/auth.go
  2. 1
      backend/app/dto/setting.go
  3. 39
      backend/app/service/auth.go
  4. 10
      backend/app/service/setting.go
  5. 1
      backend/init/migration/migrate.go
  6. 13
      backend/init/migration/migrations/init.go
  7. 3
      backend/init/router/router.go
  8. 18
      backend/middleware/safety.go
  9. 3
      backend/router/ro_base.go
  10. 2
      cmd/server/cmd/user-info.go
  11. 4
      frontend/src/api/helper/check-status.ts
  12. 11
      frontend/src/api/index.ts
  13. 1
      frontend/src/api/interface/setting.ts
  14. 6
      frontend/src/api/modules/auth.ts
  15. 2
      frontend/src/components/app-layout/menu/index.vue
  16. 11
      frontend/src/lang/modules/en.ts
  17. 11
      frontend/src/lang/modules/zh.ts
  18. 3
      frontend/src/routers/index.ts
  19. 2
      frontend/src/routers/router.ts
  20. 1
      frontend/src/store/index.ts
  21. 1
      frontend/src/store/interface/index.ts
  22. 57
      frontend/src/views/login/index.vue
  23. 4
      frontend/src/views/setting/panel/index.vue
  24. 2
      frontend/src/views/setting/panel/password/index.vue
  25. 73
      frontend/src/views/setting/safe/index.vue

39
backend/app/api/v1/auth.go

@ -1,8 +1,6 @@
package v1
import (
"errors"
"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/model"
@ -102,44 +100,17 @@ func (b *BaseApi) Captcha(c *gin.Context) {
// @Summary Load safety status
// @Description 获取系统安全登录状态
// @Success 200
// @Failure 402
// @Router /auth/status [get]
func (b *BaseApi) GetSafetyStatus(c *gin.Context) {
if err := authService.SafetyStatus(c); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) SafeEntrance(c *gin.Context) {
code, exist := c.Params.Get("code")
if !exist {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, errors.New("missing code"))
return
}
ok, err := authService.VerifyCode(code)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, errors.New("missing code"))
return
}
if !ok {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, errors.New("missing code"))
return
}
if err := authService.SafeEntrance(c, code); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, errors.New("missing code"))
return
}
helper.SuccessWithData(c, nil)
// @Router /auth/issafety [get]
func (b *BaseApi) CheckIsSafety(c *gin.Context) {
code := c.DefaultQuery("code", "")
helper.SuccessWithData(c, authService.CheckIsSafety(code))
}
// @Tags Auth
// @Summary Check is First login
// @Description 判断是否为首次登录
// @Success 200
// @Router /auth/status [get]
// @Router /auth/isfirst [get]
func (b *BaseApi) CheckIsFirstLogin(c *gin.Context) {
helper.SuccessWithData(c, authService.CheckIsFirst())
}

1
backend/app/dto/setting.go

@ -16,6 +16,7 @@ type SettingInfo struct {
Language string `json:"language"`
ServerPort string `json:"serverPort"`
SecurityEntranceStatus string `json:"securityEntranceStatus"`
SecurityEntrance string `json:"securityEntrance"`
ExpirationDays string `json:"expirationDays"`
ExpirationTime string `json:"expirationTime"`

39
backend/app/service/auth.go

@ -19,11 +19,10 @@ import (
type AuthService struct{}
type IAuthService interface {
SafetyStatus(c *gin.Context) error
CheckIsSafety(code string) bool
CheckIsFirst() bool
InitUser(c *gin.Context, req dto.InitUser) error
VerifyCode(code string) (bool, error)
SafeEntrance(c *gin.Context, code string) error
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
LogOut(c *gin.Context) error
MFALogin(c *gin.Context, info dto.MFALogin) (*dto.UserLoginInfo, error)
@ -33,22 +32,6 @@ func NewIAuthService() IAuthService {
return &AuthService{}
}
func (u *AuthService) SafeEntrance(c *gin.Context, code string) error {
codeWithMD5 := encrypt.Md5(code)
cookieValue, _ := encrypt.StringEncrypt(codeWithMD5)
c.SetCookie(codeWithMD5, cookieValue, 604800, "", "", false, false)
expiredSetting, err := settingRepo.Get(settingRepo.WithByKey("ExpirationDays"))
if err != nil {
return err
}
timeout, _ := strconv.Atoi(expiredSetting.Value)
if err := settingRepo.Update("ExpirationTime", time.Now().AddDate(0, 0, timeout).Format("2006-01-02 15:04:05")); err != nil {
return err
}
return nil
}
func (u *AuthService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error) {
nameSetting, err := settingRepo.Get(settingRepo.WithByKey("UserName"))
if err != nil {
@ -164,23 +147,19 @@ func (u *AuthService) VerifyCode(code string) (bool, error) {
return setting.Value == code, nil
}
func (u *AuthService) SafetyStatus(c *gin.Context) error {
setting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
func (u *AuthService) CheckIsSafety(code string) bool {
status, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntranceStatus"))
if err != nil {
return err
return false
}
codeWithEcrypt, err := c.Cookie(encrypt.Md5(setting.Value))
if err != nil {
return err
if status.Value == "disable" {
return true
}
code, err := encrypt.StringDecrypt(codeWithEcrypt)
setting, err := settingRepo.Get(settingRepo.WithByKey("SecurityEntrance"))
if err != nil {
return err
}
if code != encrypt.Md5(setting.Value) {
return errors.New("code not match")
return false
}
return nil
return setting.Value == code
}
func (u *AuthService) CheckIsFirst() bool {

10
backend/app/service/setting.go

@ -57,6 +57,16 @@ func (u *SettingService) Update(key, value string) error {
return err
}
}
if key == "SecurityEntrance" {
if err := settingRepo.Update("SecurityEntranceStatus", "enable"); err != nil {
return err
}
}
if key == "SecurityEntranceStatus" {
if err := settingRepo.Update("SecurityEntrance", ""); err != nil {
return err
}
}
if err := settingRepo.Update(key, value); err != nil {
return err
}

1
backend/init/migration/migrate.go

@ -25,6 +25,7 @@ func Init() {
migrations.UpdateTableApp,
migrations.UpdateTableHost,
migrations.UpdateTableWebsite,
migrations.AddEntranceStatus,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

13
backend/init/migration/migrations/init.go

@ -287,3 +287,16 @@ var UpdateTableWebsite = &gormigrate.Migration{
return nil
},
}
var AddEntranceStatus = &gormigrate.Migration{
ID: "20230414-add-entrance-status",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "SecurityEntranceStatus", Value: "disable"}).Error; err != nil {
return err
}
if err := tx.Model(&model.Setting{}).Where("key = ?", "SecurityEntrance").Updates(map[string]interface{}{"value": ""}).Error; err != nil {
return err
}
return tx.AutoMigrate(&model.Website{})
},
}

3
backend/init/router/router.go

@ -6,7 +6,6 @@ import (
"github.com/gin-contrib/gzip"
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/middleware"
@ -53,8 +52,6 @@ func Routers() *gin.Engine {
setWebStatic(Router)
Router.Use(i18n.GinI18nLocalize())
Router.GET("/api/v1/info", v1.ApiGroupApp.BaseApi.GetSafetyStatus)
Router.GET("/api/v1/:code", v1.ApiGroupApp.BaseApi.SafeEntrance)
Router.SetFuncMap(template.FuncMap{
"Localize": ginI18n.GetMessage,

18
backend/middleware/safety.go

@ -1,18 +0,0 @@
package middleware
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
func SafetyAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if err := service.NewIAuthService().SafetyStatus(c); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrUnSafety, constant.ErrTypeNotSafety, nil)
return
}
c.Next()
}
}

3
backend/router/ro_base.go

@ -14,7 +14,8 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) {
baseRouter.GET("/captcha", baseApi.Captcha)
baseRouter.POST("/mfalogin", baseApi.MFALogin)
baseRouter.POST("/login", baseApi.Login)
baseRouter.GET("/status", baseApi.CheckIsFirstLogin)
baseRouter.GET("/isfirst", baseApi.CheckIsFirstLogin)
baseRouter.GET("/issafety", baseApi.CheckIsSafety)
baseRouter.POST("/init", baseApi.InitUserInfo)
baseRouter.POST("/logout", baseApi.LogOut)
baseRouter.GET("/demo", baseApi.CheckIsDemo)

2
cmd/server/cmd/user-info.go

@ -39,6 +39,7 @@ var userinfoCmd = &cobra.Command{
user := getSettingByKey(db, "UserName")
password := getSettingByKey(db, "Password")
port := getSettingByKey(db, "ServerPort")
entrance := getSettingByKey(db, "SecurityEntrance")
enptrySetting := getSettingByKey(db, "EncryptKey")
p := ""
@ -49,6 +50,7 @@ var userinfoCmd = &cobra.Command{
p = password
}
fmt.Printf("entrance: %s\n", entrance)
fmt.Printf("username: %s\n", user)
fmt.Printf("password: %s\n", p)
fmt.Printf("port: %s\n", port)

4
frontend/src/api/helper/check-status.ts

@ -1,6 +1,8 @@
import i18n from '@/lang';
import router from '@/routers';
import { MsgError } from '@/utils/message';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
export const checkStatus = (status: number, msg: string): void => {
switch (status) {
@ -11,7 +13,7 @@ export const checkStatus = (status: number, msg: string): void => {
MsgError(msg ? msg : i18n.global.t('commons.res.notFound'));
break;
case 403:
router.replace({ path: '/login' });
router.replace({ path: '/login', params: { code: globalStore.entrance } });
MsgError(msg ? msg : i18n.global.t('commons.res.forbidden'));
break;
case 500:

11
frontend/src/api/index.ts

@ -43,17 +43,12 @@ class RequestHttp {
globalStore.setCsrfToken(response.headers['x-csrf-token']);
}
if (data.code == ResultEnum.OVERDUE || data.code == ResultEnum.FORBIDDEN) {
router.replace({
path: '/login',
router.push({
name: 'login',
params: { code: globalStore.entrance },
});
return Promise.reject(data);
}
if (data.code == ResultEnum.UNSAFETY) {
router.replace({
path: '/login',
});
return data;
}
if (data.code == ResultEnum.EXPIRED) {
router.push({ name: 'Expired' });
return data;

1
frontend/src/api/interface/setting.ts

@ -15,6 +15,7 @@ export namespace Setting {
language: string;
serverPort: number;
securityEntranceStatus: string;
securityEntrance: string;
expirationDays: number;
expirationTime: string;

6
frontend/src/api/modules/auth.ts

@ -26,7 +26,11 @@ export const loginStatus = () => {
};
export const checkIsFirst = () => {
return http.get<boolean>('/auth/status');
return http.get<boolean>('/auth/isfirst');
};
export const checkIsSafety = (code: string) => {
return http.get<boolean>(`/auth/issafety?code=${code}`);
};
export const initUser = (params: Login.InitUser) => {

2
frontend/src/components/app-layout/menu/index.vue

@ -80,7 +80,7 @@ const logout = () => {
})
.then(() => {
systemLogOut();
router.push({ name: 'login' });
router.push({ name: 'login', params: { code: globalStore.entrance } });
globalStore.setLogStatus(false);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})

11
frontend/src/lang/modules/en.ts

@ -114,15 +114,8 @@ const message = {
errorMfaInfo: 'Incorrect authentication information, please try again!',
captchaHelper: 'Captcha',
errorCaptcha: 'Captcha code error!',
safeEntrance: 'Please use the correct entry to log in to the panel',
reason: 'Cause of error:',
reasonHelper:
'At present, the newly installed machine has enabled the security entrance login. The newly installed machine will have a random 8-character security entrance name, which can also be modified in the panel Settings. If you do not record or do not remember, you can use the following methods to solve the problem',
solution: 'The solution:',
solutionHelper:
'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',
safeEntrance:
'The command "1pctl user-info" can be used in SSH terminal to view the panel entrance as the secure login has been enabled in the current environment.',
codeInput: 'Please enter the 6-digit verification code of the MFA validator',
mfaTitle: 'MFA Certification',
mfaCode: 'MFA verification code',

11
frontend/src/lang/modules/zh.ts

@ -118,13 +118,7 @@ const message = {
errorMfaInfo: '错误的验证信息请重试',
captchaHelper: '验证码',
errorCaptcha: '验证码错误',
safeEntrance: '请使用正确的入口登录面板',
reason: '错误原因',
reasonHelper:
'当前新安装的已经开启了安全入口登录新装机器都会随机一个8位字符的安全入口名称亦可以在面板设置处修改如您没记录或不记得了可以使用以下方式解决',
solution: '解决方法',
solutionHelper: ' SSH 终端输入以下一种命令来解决 1.查看面板入口/etc/init.d/bt default',
warnning: '注意关闭安全入口将使您的面板登录地址被直接暴露在互联网上非常危险请谨慎操作',
safeEntrance: '当前环境已经开启了安全入口登录 SSH 终端输入以下命令来查看面板入口: 1pctl user-info',
codeInput: '请输入 MFA 验证器的 6 位验证码',
mfaTitle: 'MFA 认证',
mfaCode: 'MFA 验证码',
@ -806,6 +800,9 @@ const message = {
portHelper: '建议端口范围8888 - 65535注意有安全组的服务器请提前在安全组放行新端口',
portChange: '端口修改',
portChangeHelper: '服务端口修改需要重启服务是否继续',
entrance: '安全入口',
entranceHelper: '开启安全入口后只能通过指定安全入口登录面板',
entranceError: '请输入 8 位安全登录入口仅支持输入数字或字母',
theme: '主题颜色',
componentSize: '组件大小',
dark: '暗色',

3
frontend/src/routers/index.ts

@ -17,7 +17,8 @@ router.beforeEach((to, from, next) => {
const globalStore = GlobalStore();
if (!globalStore.isLogin) {
next({
path: '/login',
name: 'login',
params: { code: globalStore.entrance },
});
NProgress.done();
return;

2
frontend/src/routers/router.ts

@ -58,7 +58,7 @@ menuList.unshift(homeRouter);
export const routes: RouteRecordRaw[] = [
homeRouter,
{
path: '/login',
path: '/login/:code?',
name: 'login',
props: true,
component: () => import('@/views/login/index.vue'),

1
frontend/src/store/index.ts

@ -12,6 +12,7 @@ export const GlobalStore = defineStore({
loadingText: '',
isLogin: false,
csrfToken: '',
entrance: '',
language: '',
themeConfig: {
panelName: '',

1
frontend/src/store/interface/index.ts

@ -11,6 +11,7 @@ export interface GlobalState {
isLoading: boolean;
loadingText: string;
isLogin: boolean;
entrance: string;
csrfToken: string;
language: string; // zh | en
// assemblySize: string; // small | default | large

57
frontend/src/views/login/index.vue

@ -1,6 +1,6 @@
<template>
<div>
<div class="login-backgroud" v-if="statusCode == 1">
<div class="login-backgroud" v-if="isSafety">
<div class="login-wrapper">
<div :class="screenWidth > 1110 ? 'left inline-block' : ''">
<div class="login-title">
@ -15,36 +15,36 @@
</div>
</div>
</div>
<div style="margin-left: 50px" v-if="statusCode == -1">
<h1>{{ $t('commons.login.safeEntrance') }}</h1>
<div style="line-height: 30px">
<span style="font-weight: 500">{{ $t('commons.login.reason') }}</span>
<span>
{{ $t('commons.login.reasonHelper') }}
</span>
</div>
<div style="line-height: 30px">
<span style="font-weight: 500">{{ $t('commons.login.solution') }}</span>
<span>{{ $t('commons.login.solutionHelper') }}</span>
</div>
<div style="line-height: 30px">
<span style="color: red">
{{ $t('commons.login.warnning') }}
</span>
<div style="margin-left: 50px" v-if="!isSafety">
<div class="not-found">
<h1>404 NOT FOUND</h1>
<p>{{ $t('commons.login.safeEntrance') }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="login">
import { checkIsSafety } from '@/api/modules/auth';
import LoginForm from './components/login-form.vue';
import { ref, onMounted } from 'vue';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const statusCode = ref<number>(0);
const isSafety = ref(false);
const screenWidth = ref(null);
interface Props {
code: string;
}
const mySafetyCode = withDefaults(defineProps<Props>(), {
code: '',
});
const getStatus = async () => {
statusCode.value = 1;
const res = await checkIsSafety(mySafetyCode.code);
isSafety.value = res.data;
globalStore.entrance = mySafetyCode.code;
};
onMounted(() => {
@ -138,4 +138,23 @@ onMounted(() => {
}
}
}
.not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
.h1 {
font-size: 5rem;
margin: 0 0 1rem;
}
.p {
font-size: 1.2rem;
max-width: 500px;
text-align: center;
margin: 0 0 2rem;
}
}
</style>

4
frontend/src/views/setting/panel/index.vue

@ -194,7 +194,7 @@ const onSaveUserName = async (formEl: FormInstance | undefined, key: string, val
await logOutApi();
loading.value = false;
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
router.push({ name: 'login', params: { code: '' } });
router.push({ name: 'login', params: { code: globalStore.entrance } });
globalStore.setLogStatus(false);
return;
})
@ -250,7 +250,7 @@ const onSave = async (formEl: FormInstance | undefined, key: string, val: any) =
await logOutApi();
loading.value = false;
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
router.push({ name: 'login', params: { code: '' } });
router.push({ name: 'login', params: { code: globalStore.entrance } });
globalStore.setLogStatus(false);
return;
}

2
frontend/src/views/setting/panel/password/index.vue

@ -119,7 +119,7 @@ const submitChangePassword = async (formEl: FormInstance | undefined) => {
passwordVisiable.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
await logOutApi();
router.push({ name: 'login', params: { code: '' } });
router.push({ name: 'login', params: { code: globalStore.entrance } });
globalStore.setLogStatus(false);
})
.catch(() => {

73
frontend/src/views/setting/safe/index.vue

@ -20,6 +20,33 @@
</el-input>
</el-form-item>
<el-form-item :label="$t('setting.entrance')" required>
<el-switch
@change="handleEntrance"
v-model="form.securityEntranceStatus"
active-value="enable"
inactive-value="disable"
/>
<span class="input-help">
{{ $t('setting.entranceHelper') }}
</span>
<el-input
@blur="codeError = false"
v-if="isEntranceShow"
clearable
v-model.number="form.securityEntrance"
>
<template #append>
<el-button style="width: 85px" @click="onSaveEntrance" icon="Collection">
{{ $t('commons.button.save') }}
</el-button>
</template>
</el-input>
<span class="input-error" v-if="codeError">
{{ $t('setting.entranceError') }}
</span>
</el-form-item>
<el-form-item
:label="$t('setting.expirationTime')"
prop="expirationTime"
@ -142,10 +169,13 @@ import i18n from '@/lang';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { dateFormatSimple } from '@/utils/util';
import { MsgError, MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const loading = ref(false);
const form = reactive({
serverPort: 9999,
securityEntranceStatus: 'disable',
securityEntrance: '',
expirationDays: 0,
expirationTime: '',
@ -163,6 +193,8 @@ const timeoutForm = reactive({
const search = async () => {
const res = await getSettingInfo();
form.serverPort = Number(res.data.serverPort);
form.securityEntranceStatus = res.data.securityEntranceStatus;
isEntranceShow.value = res.data.securityEntranceStatus === 'enable';
form.securityEntrance = res.data.securityEntrance;
form.expirationDays = Number(res.data.expirationDays);
form.expirationTime = res.data.expirationTime;
@ -171,6 +203,9 @@ const search = async () => {
form.mfaSecret = res.data.mfaSecret;
};
const isEntranceShow = ref(false);
const codeError = ref(false);
const isMFAShow = ref<boolean>(false);
const otp = reactive<Setting.MFAInfo>({
secret: '',
@ -259,6 +294,44 @@ const handleMFA = async () => {
}
};
const handleEntrance = async () => {
if (form.securityEntranceStatus === 'enable') {
isEntranceShow.value = true;
} else {
isEntranceShow.value = false;
loading.value = true;
await updateSetting({ key: 'SecurityEntranceStatus', value: 'disable' })
.then(() => {
globalStore.entrance = '';
loading.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
}
};
const onSaveEntrance = async () => {
const reg = /^[A-Za-z0-9]{8}$/;
if ((!reg.test(form.securityEntrance) && form.securityEntrance !== '') || form.securityEntrance === '') {
codeError.value = true;
return;
}
loading.value = true;
await updateSetting({ key: 'SecurityEntrance', value: form.securityEntrance })
.then(() => {
globalStore.entrance = form.securityEntrance;
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const handleClose = () => {
timeoutVisiable.value = false;
};

Loading…
Cancel
Save