✨ 支持MFA2二次验证
This commit is contained in:
parent
f0b492da26
commit
678a1e4d04
@ -1,7 +1,7 @@
|
||||
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-xx)
|
||||
## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-24)
|
||||
|
||||
* 重构本地数据库存储方式(性能提升一个level~)
|
||||
* 支持MFA2二次验证
|
||||
* 支持MFA2二次登录验证
|
||||
* 优化了一些页面在移动端的展示
|
||||
* 修复偶现刷新页面需重新登录的bug
|
||||
|
||||
|
@ -24,7 +24,7 @@ let loginCountDown = forbidTimer
|
||||
let forbidLogin = false
|
||||
|
||||
const login = async ({ res, request }) => {
|
||||
let { body: { loginName, ciphertext, jwtExpires }, ip: clientIp } = request
|
||||
let { body: { loginName, ciphertext, jwtExpires, mfa2Token }, ip: clientIp } = request
|
||||
if (!loginName && !ciphertext) return res.fail({ msg: '请求非法!' })
|
||||
if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
||||
loginErrCount++
|
||||
@ -55,10 +55,13 @@ const login = async ({ res, request }) => {
|
||||
|
||||
// 登录流程
|
||||
try {
|
||||
// console.log('ciphertext', ciphertext)
|
||||
let loginPwd = await RSADecryptAsync(ciphertext)
|
||||
// console.log('Decrypt解密password:', loginPwd)
|
||||
let { user, pwd } = await keyDB.findOneAsync({})
|
||||
let { user, pwd, enableMFA2, secret } = await keyDB.findOneAsync({})
|
||||
if (enableMFA2) {
|
||||
const isValid = speakeasy.totp.verify({ secret, encoding: 'base32', token: mfa2Token, window: 1 })
|
||||
console.log('MFA2 verfify:', isValid)
|
||||
if (!isValid) return res.fail({ msg: '验证失败' })
|
||||
}
|
||||
if (loginName === user && loginPwd === 'admin' && pwd === 'admin') {
|
||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认用户名和密码' })
|
||||
@ -68,8 +71,8 @@ const login = async ({ res, request }) => {
|
||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功' })
|
||||
} catch (error) {
|
||||
console.log('解密失败:', error)
|
||||
res.fail({ msg: '解密失败, 请查看服务端日志' })
|
||||
console.log('登录失败:', error.message)
|
||||
res.fail({ msg: '登录失败, 请查看服务端日志' })
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +90,7 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
||||
const { ip, country, city } = clientIPInfo || {}
|
||||
consola.info('登录成功:', new Date(), { ip, country, city })
|
||||
|
||||
// 邮件登录通知
|
||||
// 登录通知
|
||||
sendNoticeAsync('login', '登录提醒', `地点:${ country + city }\nIP: ${ ip }`)
|
||||
|
||||
await logDB.insertAsync({ ip, country, city, date: Date.now(), type: 'login' })
|
||||
@ -140,12 +143,7 @@ const enableMFA2 = async ({ res, request }) => {
|
||||
if (!token) return res.fail({ data: false, msg: '参数错误' })
|
||||
try {
|
||||
// const isValid = authenticator.verify({ token, secret: tempSecret })
|
||||
const isValid = speakeasy.totp.verify({
|
||||
secret: tempSecret,
|
||||
encoding: 'base32',
|
||||
token,
|
||||
window: 1
|
||||
})
|
||||
const isValid = speakeasy.totp.verify({ secret: tempSecret, encoding: 'base32', token, window: 1 })
|
||||
if (!isValid) return res.fail({ msg: '验证失败' })
|
||||
const keyConfig = await keyDB.findOneAsync({})
|
||||
keyConfig.enableMFA2 = true
|
||||
|
@ -10,7 +10,7 @@ const enumLoginCode = {
|
||||
ERROR_TOKEN: -2
|
||||
}
|
||||
|
||||
// 校验token与登录IP
|
||||
// 校验token
|
||||
const verifyAuthSync = async (token, clientIp) => {
|
||||
consola.info('verifyAuthSync IP:', clientIp)
|
||||
try {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.2.8",
|
||||
"version": "2.3.0",
|
||||
"description": "easynode-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -44,6 +44,17 @@
|
||||
<el-form-item v-show="false" prop="pwd" label="密码">
|
||||
<el-input v-model.trim="loginForm.pwd" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="mfa2Token" label="MFA2验证码">
|
||||
<el-input
|
||||
v-model.trim.number="loginForm.mfa2Token"
|
||||
type="text"
|
||||
placeholder="MFA2应用上的6位数字(未设置可忽略)"
|
||||
autocomplete="off"
|
||||
:trigger-on-focus="false"
|
||||
clearable
|
||||
autofocus
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="jwtExpires" label="有效期">
|
||||
<el-radio-group v-model="expireTime" class="login-indate">
|
||||
<el-radio :value="expireEnum.ONE_SESSION">一次性会话</el-radio>
|
||||
@ -70,10 +81,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
||||
import { RSAEncrypt } from '@utils/index.js'
|
||||
// import { useRouter } from 'vue-router'
|
||||
// import useStore from '@store/index'
|
||||
|
||||
// const router = useRouter()
|
||||
const { proxy: { $store, $api, $message, $messageBox, $router } } = getCurrentInstance()
|
||||
|
||||
const expireEnum = reactive({
|
||||
@ -88,16 +96,18 @@ const loading = ref(false)
|
||||
const loginForm = reactive({
|
||||
loginName: '',
|
||||
pwd: '',
|
||||
jwtExpires: 1
|
||||
jwtExpires: 1,
|
||||
mfa2Token: ''
|
||||
})
|
||||
const rules = reactive({
|
||||
loginName: { required: true, message: '需输入用户名', trigger: 'change' },
|
||||
pwd: { required: true, message: '需输入密码', trigger: 'change' }
|
||||
pwd: { required: true, message: '需输入密码', trigger: 'change' },
|
||||
mfa2Token: { required: false, message: '需输入密码', trigger: 'change' }
|
||||
})
|
||||
|
||||
const handleLogin = () => {
|
||||
loginFormRefs.value.validate().then(() => {
|
||||
let { jwtExpires, loginName, pwd } = loginForm
|
||||
loginFormRefs.value.validate().then(async () => {
|
||||
let { jwtExpires, loginName, pwd, mfa2Token } = loginForm
|
||||
switch (expireTime.value) {
|
||||
case expireEnum.ONE_SESSION:
|
||||
jwtExpires = '1h' // 会话登录token1小时有效期,浏览器窗口关闭则立即失效
|
||||
@ -112,14 +122,23 @@ const handleLogin = () => {
|
||||
const ciphertext = RSAEncrypt(pwd)
|
||||
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
||||
loading.value = true
|
||||
$api.login({ loginName, ciphertext, jwtExpires })
|
||||
.then(({ data, msg }) => {
|
||||
try {
|
||||
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token })
|
||||
const { token } = data
|
||||
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
|
||||
$store.setUser(loginName)
|
||||
$message.success({ message: msg || 'success', center: true })
|
||||
loginSuccess()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loginSuccess = () => {
|
||||
let { loginName, pwd } = loginForm
|
||||
if (loginName === 'admin' && pwd === 'admin') {
|
||||
$messageBox.confirm('请立即修改初始用户名及密码!防止恶意扫描!', '警告', {
|
||||
$messageBox.confirm('请立即修改初始用户名及密码!防止恶意扫描!', 'Warning', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false,
|
||||
type: 'warning'
|
||||
@ -130,11 +149,6 @@ const handleLogin = () => {
|
||||
} else {
|
||||
$router.push('/')
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -68,6 +68,7 @@
|
||||
class="mfa2_input"
|
||||
clearable
|
||||
placeholder=""
|
||||
autofocus
|
||||
@keyup.enter="handleEnableMFA2"
|
||||
/>
|
||||
<el-button type="primary" @click="handleEnableMFA2">保存</el-button>
|
||||
@ -130,7 +131,6 @@ const handleMFA2 = async () => {
|
||||
startEnableMFA2.value = false
|
||||
let { data } = await $api.getMFA2QR()
|
||||
MFA2Data.value = data
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
const handleEnableMFA2 = async () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user