Skip to content

Commit

Permalink
✨ 支持MFA2二次验证
Browse files Browse the repository at this point in the history
  • Loading branch information
chaos-zhu committed Oct 23, 2024
1 parent f0b492d commit 678a1e4
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 47 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
24 changes: 11 additions & 13 deletions server/app/controller/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down Expand Up @@ -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: '登录成功,请及时修改默认用户名和密码' })
Expand All @@ -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: '登录失败, 请查看服务端日志' })
}
}

Expand All @@ -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' })
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/app/utils/verify-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const enumLoginCode = {
ERROR_TOKEN: -2
}

// 校验token与登录IP
// 校验token
const verifyAuthSync = async (token, clientIp) => {
consola.info('verifyAuthSync IP:', clientIp)
try {
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.2.8",
"version": "2.3.0",
"description": "easynode-web",
"private": true,
"scripts": {
Expand Down
72 changes: 43 additions & 29 deletions web/src/views/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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({
Expand All @@ -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小时有效期,浏览器窗口关闭则立即失效
Expand All @@ -112,31 +122,35 @@ 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 }) => {
const { token } = data
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
$store.setUser(loginName)
$message.success({ message: msg || 'success', center: true })
if (loginName === 'admin' && pwd === 'admin') {
$messageBox.confirm('请立即修改初始用户名及密码!防止恶意扫描!', '警告', {
confirmButtonText: '确定',
showCancelButton: false,
type: 'warning'
})
.then(async () => {
$router.push('/setting')
})
} else {
$router.push('/')
}
})
.finally(() => {
loading.value = false
})
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('请立即修改初始用户名及密码!防止恶意扫描!', 'Warning', {
confirmButtonText: '确定',
showCancelButton: false,
type: 'warning'
})
.then(async () => {
$router.push('/setting')
})
} else {
$router.push('/')
}
}
onMounted(async () => {
if (localStorage.getItem('jwtExpires')) loginForm.jwtExpires = Number(localStorage.getItem('jwtExpires'))
const { data } = await $api.getPubPem()
Expand Down
2 changes: 1 addition & 1 deletion web/src/views/setting/components/user.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
class="mfa2_input"
clearable
placeholder=""
autofocus
@keyup.enter="handleEnableMFA2"
/>
<el-button type="primary" @click="handleEnableMFA2">保存</el-button>
Expand Down Expand Up @@ -130,7 +131,6 @@ const handleMFA2 = async () => {
startEnableMFA2.value = false
let { data } = await $api.getMFA2QR()
MFA2Data.value = data
console.log(data)
}
const handleEnableMFA2 = async () => {
Expand Down

0 comments on commit 678a1e4

Please sign in to comment.