- 优化双因素认证流程
This commit is contained in:
parent
53e4048944
commit
7c4860ce28
2
main.go
2
main.go
@ -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())
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user