支持MFA2二次验证

This commit is contained in:
chaos-zhu 2024-10-23 23:59:38 +08:00
parent f0b492da26
commit 678a1e4d04
6 changed files with 59 additions and 47 deletions

View File

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

View File

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

View File

@ -10,7 +10,7 @@ const enumLoginCode = {
ERROR_TOKEN: -2
}
// 校验token与登录IP
// 校验token
const verifyAuthSync = async (token, clientIp) => {
consola.info('verifyAuthSync IP', clientIp)
try {

View File

@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.2.8",
"version": "2.3.0",
"description": "easynode-web",
"private": true,
"scripts": {

View File

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

View File

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