- 优化双因素认证流程

This commit is contained in:
dushixiang 2021-01-24 17:16:33 +08:00
parent 53e4048944
commit 7c4860ce28
5 changed files with 138 additions and 24 deletions

View File

@ -23,7 +23,7 @@ import (
"time" "time"
) )
const Version = "v0.0.9" const Version = "v0.1.0"
func main() { func main() {
log.Fatal(Run()) log.Fatal(Run())

View File

@ -156,7 +156,7 @@ func ConfirmTOTPEndpoint(c echo.Context) error {
return Success(c, nil) return Success(c, nil)
} }
func ResetTOTPEndpoint(c echo.Context) error { func ReloadTOTPEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
key, err := totp.NewTOTP(totp.GenerateOpts{ key, err := totp.NewTOTP(totp.GenerateOpts{
@ -183,6 +183,15 @@ func ResetTOTPEndpoint(c echo.Context) error {
}) })
} }
func ResetTOTPEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
u := &model.User{
TOTPSecret: "-",
}
model.UpdateUserById(u, account.ID)
return Success(c, "")
}
func ChangePasswordEndpoint(c echo.Context) error { func ChangePasswordEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
@ -208,7 +217,28 @@ func ChangePasswordEndpoint(c echo.Context) error {
return LogoutEndpoint(c) return LogoutEndpoint(c)
} }
type AccountInfo struct {
Id string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Type string `json:"type"`
EnableTotp bool `json:"enableTotp"`
}
func InfoEndpoint(c echo.Context) error { func InfoEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
return Success(c, account)
user, err := model.FindUserById(account.ID)
if err != nil {
return err
}
info := AccountInfo{
Id: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Type: user.Type,
EnableTotp: user.TOTPSecret != "" && user.TOTPSecret != "-",
}
return Success(c, info)
} }

View File

@ -38,6 +38,7 @@ func SetupRoutes() *echo.Echo {
e.POST("/logout", LogoutEndpoint) e.POST("/logout", LogoutEndpoint)
e.POST("/change-password", ChangePasswordEndpoint) e.POST("/change-password", ChangePasswordEndpoint)
e.GET("/reload-totp", ReloadTOTPEndpoint)
e.POST("/reset-totp", ResetTOTPEndpoint) e.POST("/reset-totp", ResetTOTPEndpoint)
e.POST("/confirm-totp", ConfirmTOTPEndpoint) e.POST("/confirm-totp", ConfirmTOTPEndpoint)
e.GET("/info", InfoEndpoint) e.GET("/info", InfoEndpoint)

View File

@ -1,6 +1,6 @@
{ {
"name": "next-terminal", "name": "next-terminal",
"version": "0.0.9", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.3.0", "@ant-design/icons": "^4.3.0",

View File

@ -1,9 +1,10 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {Button, Card, Form, Image, Input, Layout, PageHeader} from "antd"; import {Button, Card, Form, Image, Input, Layout, Modal, PageHeader, Result, Space} from "antd";
import {itemRender} from '../../utils/utils' import {itemRender} from '../../utils/utils'
import request from "../../common/request"; import request from "../../common/request";
import {message} from "antd/es"; import {message} from "antd/es";
import Logout from "./Logout"; import Logout from "./Logout";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
const {Content} = Layout; const {Content} = Layout;
const {Meta} = Card; const {Meta} = Card;
@ -27,13 +28,34 @@ const formTailLayout = {
labelCol: {span: 3}, labelCol: {span: 3},
wrapperCol: {span: 6, offset: 3}, wrapperCol: {span: 6, offset: 3},
}; };
const {confirm} = Modal;
class Info extends Component { class Info extends Component {
state = {} state = {
user: {
enableTotp: false
}
}
passwordFormRef = React.createRef(); passwordFormRef = React.createRef();
componentDidMount() {
this.loadInfo();
}
loadInfo = async () => {
let result = await request.get('/info');
if (result['code'] === 1) {
this.setState({
user: result['data']
})
sessionStorage.setItem('user', JSON.stringify(result['data']));
} else {
message.error(result['message']);
}
}
onNewPasswordChange(value) { onNewPasswordChange(value) {
this.setState({ this.setState({
'newPassword': value.target.value 'newPassword': value.target.value
@ -75,6 +97,7 @@ class Info extends Component {
let result = await request.post('/confirm-totp', values); let result = await request.post('/confirm-totp', values);
if (result.code === 1) { if (result.code === 1) {
message.success('TOTP启用成功'); message.success('TOTP启用成功');
await this.loadInfo();
this.setState({ this.setState({
qr: "", qr: "",
secret: "" secret: ""
@ -85,7 +108,7 @@ class Info extends Component {
} }
resetTOTP = async () => { resetTOTP = async () => {
let result = await request.post('/reset-totp'); let result = await request.get('/reload-totp');
if (result.code === 1) { if (result.code === 1) {
this.setState({ this.setState({
qr: result.data.qr, qr: result.data.qr,
@ -167,33 +190,93 @@ class Info extends Component {
</Content> </Content>
<Content className="site-layout-background page-content"> <Content className="site-layout-background page-content">
<h1>双因素认证</h1> <h1>双因素认证</h1>
<Form hidden={this.state.qr} onFinish={this.resetTOTP}> <Form hidden={this.state.qr}>
<Form.Item {...formTailLayout}> <Form.Item>
<Button type="primary" htmlType="submit"> {
重置 TOTP this.state.user.enableTotp ?
</Button> <Result
status="success"
title="您已成功开启双因素认证!"
subTitle="多因素认证-MFA二次认证-登录身份鉴别,访问控制更安全。"
extra={[
<Button type="primary" key="console" danger onClick={() => {
confirm({
title: '您确认要解除双因素认证吗?',
icon: <ExclamationCircleOutlined/>,
content: '解除之后可能存在系统账号被暴力破解的风险。',
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
let result = await request.post('/reset-totp');
if (result.code === 1) {
message.success('双因素认证解除成功');
await this.loadInfo();
} else {
message.error(result.message);
}
},
onCancel() {
console.log('Cancel');
},
})
}}>
解除绑定
</Button>,
<Button key="re-bind" onClick={this.resetTOTP}>重新绑定</Button>,
]}
/> :
<Result
status="warning"
title="您还未开启双因素认证!"
subTitle="系统账号存在被暴力破解的风险。"
extra={
<Button type="primary" key="bind" onClick={this.resetTOTP}>
去开启
</Button>
}
/>
}
</Form.Item> </Form.Item>
</Form> </Form>
<Form hidden={!this.state.qr} onFinish={this.confirmTOTP}> <Form hidden={!this.state.qr} onFinish={this.confirmTOTP}>
<Form.Item {...formItemLayout} label="二维码"> <Form.Item {...formItemLayout} label="二维码">
<Space size={12}>
<Card <Card
hoverable hoverable
style={{width: 280}} style={{width: 280}}
cover={<Image cover={<Image
style={{margin: 40, marginBottom: 20}} style={{margin: 40, marginBottom: 20}}
width={200} width={200}
src={"data:image/png;base64, " + this.state.qr} src={"data:image/png;base64, " + this.state.qr}
/>} />
> }
<Meta title="双因素认证二维码" description="有效期30秒在扫描后请尽快输入。"/> >
</Card> <Meta title="双因素认证二维码"
description="有效期30秒在扫描后请尽快输入。推荐使用Google Authenticator, Authy 或者 Microsoft Authenticator。"/>
</Card>
<Button
type="primary"
icon={<ReloadOutlined/>}
onClick={this.resetTOTP}
>
重新加载
</Button>
</Space>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...formItemLayout} {...formItemLayout}
name="totp" name="totp"
label="TOTP" label="TOTP"
rules={[]} rules={[
{
required: true,
message: '请输入双因素认证APP中显示的授权码',
},
]}
> >
<Input placeholder="请输入双因素认证APP中显示的授权码"/> <Input placeholder="请输入双因素认证APP中显示的授权码"/>
</Form.Item> </Form.Item>