feat: 添加人脸识别 MFA

This commit is contained in:
Aaron3S
2024-11-13 17:41:06 +08:00
committed by Bryan
parent b80b69dd26
commit c9f62ae5d3
2 changed files with 87 additions and 24 deletions

View File

@@ -8,7 +8,7 @@
:visible.sync="visible" :visible.sync="visible"
class="dialog-content" class="dialog-content"
v-bind="$attrs" v-bind="$attrs"
width="600px" width="740px"
@confirm="visible = false" @confirm="visible = false"
v-on="$listeners" v-on="$listeners"
> >
@@ -52,11 +52,21 @@
<el-row :gutter="24" style="margin: 0 auto;"> <el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; align-items: center; margin-bottom: 20px;"> <el-col :md="24" :sm="24" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input <el-input
v-if="subTypeSelected !== 'face'"
v-model="secretValue" v-model="secretValue"
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
:show-password="showPassword" :show-password="showPassword"
@keyup.enter.native="handleConfirm" @keyup.enter.native="handleConfirm"
/> />
<iframe
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
:src="faceCaptureUrl"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 800px;border: none;"
/>
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;"> <span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
<el-button <el-button
:disabled="smsBtnDisabled" :disabled="smsBtnDisabled"
@@ -72,9 +82,24 @@
</el-row> </el-row>
<el-row :gutter="24" style="margin: 10px auto;"> <el-row :gutter="24" style="margin: 10px auto;">
<el-col :md="24" :sm="24"> <el-col :md="24" :sm="24">
<el-button class="confirm-btn" size="mini" type="primary" @click="handleConfirm"> <el-button
v-if="subTypeSelected!=='face'"
class="confirm-btn"
size="mini"
type="primary"
@click="handleConfirm"
>
{{ this.$t('Confirm') }} {{ this.$t('Confirm') }}
</el-button> </el-button>
<el-button
v-if="subTypeSelected==='face'&&!isFaceCaptureVisible"
class="confirm-btn"
size="mini"
type="primary"
@click="handleFaceCapture"
>
开始人脸识别
</el-button>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -113,7 +138,10 @@ export default {
visible: false, visible: false,
callback: null, callback: null,
cancel: null, cancel: null,
processing: false processing: false,
isFaceCaptureVisible: false,
faceToken: null,
faceCaptureUrl: null
} }
}, },
computed: { computed: {
@@ -129,6 +157,10 @@ export default {
}, },
methods: { methods: {
handleSubTypeChange(val) { handleSubTypeChange(val) {
if (val !== 'face') {
this.isFaceCaptureVisible = false
}
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
this.smsWidth = val === 'sms' ? 6 : 0 this.smsWidth = val === 'sms' ? 6 : 0
}, },
@@ -192,6 +224,29 @@ export default {
this.$message.error(this.$tc('FailedToSendVerificationCode')) this.$message.error(this.$tc('FailedToSendVerificationCode'))
}) })
}, },
startFaceCapture() {
const url = '/api/v1/authentication/mfa/face/context/'
this.$axios.post(url).then(data => {
const token = data['token']
this.faceCaptureUrl = '/facelive/capture?token=' + token
this.isFaceCaptureVisible = true
const timer = setInterval(() => {
this.$axios.get(url + `?token=${token}`).then(data => {
if (data['is_finished']) {
clearInterval(timer)
this.isFaceCaptureVisible = false
this.handleConfirm()
}
})
}, 1000)
}).catch(() => {
this.$message.error(this.$tc('FailedToStartFaceCapture'))
})
},
handleFaceCapture() {
this.startFaceCapture()
},
handleConfirm() { handleConfirm() {
if (this.confirmTypeRequired === 'relogin') { if (this.confirmTypeRequired === 'relogin') {
return this.logout() return this.logout()
@@ -214,6 +269,8 @@ export default {
}) })
}).catch((err) => { }).catch((err) => {
this.$message.error(err.message || this.$tc('ConfirmFailed')) this.$message.error(err.message || this.$tc('ConfirmFailed'))
this.faceCaptureUrl = null
this.isFaceCaptureVisible = false
}) })
} }
} }
@@ -221,21 +278,21 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dialog-content ::v-deep .el-dialog__footer { .dialog-content ::v-deep .el-dialog__footer {
padding: 0; padding: 0;
} }
.dialog-content ::v-deep .el-dialog { .dialog-content ::v-deep .el-dialog {
padding: 8px; padding: 8px;
.el-dialog__body { .el-dialog__body {
padding-top: 30px; padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
}
} }
}
.confirm-btn { .confirm-btn {
width: 100%; width: 100%;
line-height: 20px; line-height: 20px;
} }
</style> </style>

View File

@@ -74,6 +74,11 @@ module.exports = {
target: 'http://127.0.0.1:4200', target: 'http://127.0.0.1:4200',
changeOrigin: true changeOrigin: true
}, },
'/facelive/': {
target: 'http://localhost:9999',
changeOrigin: true,
ws: true
},
'^/(core|static|media)/': { '^/(core|static|media)/': {
target: process.env.VUE_APP_CORE_HOST || 'http://127.0.0.1:8080', target: process.env.VUE_APP_CORE_HOST || 'http://127.0.0.1:8080',
changeOrigin: true changeOrigin: true
@@ -81,8 +86,7 @@ module.exports = {
}, },
after: require('./mock/mock-server.js') after: require('./mock/mock-server.js')
}, },
css: { css: {},
},
configureWebpack: { configureWebpack: {
// provide the app's title in webpack's name field, so that // provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title. // it can be accessed in index.html to inject the correct title.
@@ -90,7 +94,8 @@ module.exports = {
resolve: { resolve: {
alias: { alias: {
'@': resolve('src'), '@': resolve('src'),
elementCss: resolve('node_modules/element-ui/lib/theme-chalk/index.css'), elementCss: resolve(
'node_modules/element-ui/lib/theme-chalk/index.css'),
elementLocale: resolve('node_modules/element-ui/lib/locale/lang/en.js') elementLocale: resolve('node_modules/element-ui/lib/locale/lang/en.js')
}, },
extensions: ['.vue', '.js', '.json'] extensions: ['.vue', '.js', '.json']
@@ -148,7 +153,7 @@ module.exports = {
.end() .end()
config config
// https://webpack.js.org/configuration/devtool/#development // https://webpack.js.org/configuration/devtool/#development
.when(process.env.NODE_ENV === 'development', .when(process.env.NODE_ENV === 'development',
config => config.devtool('cheap-source-map') config => config.devtool('cheap-source-map')
) )
@@ -159,10 +164,11 @@ module.exports = {
config config
.plugin('ScriptExtHtmlWebpackPlugin') .plugin('ScriptExtHtmlWebpackPlugin')
.after('html') .after('html')
.use('script-ext-html-webpack-plugin', [{ .use('script-ext-html-webpack-plugin', [
// `runtime` must same as runtimeChunk name. default is `runtime` {
inline: /runtime\..*\.js$/ // `runtime` must same as runtimeChunk name. default is `runtime`
}]) inline: /runtime\..*\.js$/
}])
.end() .end()
config config
.optimization.splitChunks({ .optimization.splitChunks({