优化了双因素认证的流程和页面
This commit is contained in:
		@ -48,6 +48,45 @@ func LoginEndpoint(c echo.Context) error {
 | 
				
			|||||||
		return Fail(c, -1, "您输入的账号或密码不正确")
 | 
							return Fail(c, -1, "您输入的账号或密码不正确")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user.TOTPSecret != "" {
 | 
				
			||||||
 | 
							return Fail(c, 0, "")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						token := utils.UUID()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authorization := Authorization{
 | 
				
			||||||
 | 
							Token:    token,
 | 
				
			||||||
 | 
							Remember: loginAccount.Remember,
 | 
				
			||||||
 | 
							User:     user,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if authorization.Remember {
 | 
				
			||||||
 | 
							// 记住登录有效期两周
 | 
				
			||||||
 | 
							global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14))
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							global.Cache.Set(token, authorization, time.Hour*time.Duration(2))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model.UpdateUserById(&model.User{Online: true}, user.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return Success(c, token)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loginWithTotpEndpoint(c echo.Context) error {
 | 
				
			||||||
 | 
						var loginAccount LoginAccount
 | 
				
			||||||
 | 
						if err := c.Bind(&loginAccount); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := model.FindUserByUsername(loginAccount.Username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return Fail(c, -1, "您输入的账号或密码不正确")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
 | 
				
			||||||
 | 
							return Fail(c, -1, "您输入的账号或密码不正确")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) {
 | 
						if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) {
 | 
				
			||||||
		return Fail(c, -2, "您的TOTP不匹配")
 | 
							return Fail(c, -2, "您的TOTP不匹配")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ func SetupRoutes() *echo.Echo {
 | 
				
			|||||||
	e.Use(Auth)
 | 
						e.Use(Auth)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	e.POST("/login", LoginEndpoint)
 | 
						e.POST("/login", LoginEndpoint)
 | 
				
			||||||
 | 
						e.POST("/loginWithTotp", loginWithTotpEndpoint)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	e.GET("/tunnel", TunEndpoint)
 | 
						e.GET("/tunnel", TunEndpoint)
 | 
				
			||||||
	e.GET("/ssh", SSHEndpoint)
 | 
						e.GET("/ssh", SSHEndpoint)
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,7 @@ const handleError = (error) => {
 | 
				
			|||||||
const handleResult = (result) => {
 | 
					const handleResult = (result) => {
 | 
				
			||||||
    if (result['code'] === 403) {
 | 
					    if (result['code'] === 403) {
 | 
				
			||||||
        window.location.href = '#/login';
 | 
					        window.location.href = '#/login';
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,24 @@
 | 
				
			|||||||
import React, { Component } from 'react';
 | 
					import React, {Component} from 'react';
 | 
				
			||||||
import { Button, Card, Checkbox, Form, Input, Typography } from "antd";
 | 
					import {Button, Card, Checkbox, Form, Input, Modal, Typography} from "antd";
 | 
				
			||||||
import './Login.css'
 | 
					import './Login.css'
 | 
				
			||||||
import request from "../common/request";
 | 
					import request from "../common/request";
 | 
				
			||||||
import { message } from "antd/es";
 | 
					import {message} from "antd/es";
 | 
				
			||||||
import { withRouter } from "react-router-dom";
 | 
					import {withRouter} from "react-router-dom";
 | 
				
			||||||
import { OneToOneOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
 | 
					import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {Title} = Typography;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { Title } = Typography;
 | 
					 | 
				
			||||||
class LoginForm extends Component {
 | 
					class LoginForm extends Component {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    formRef = React.createRef()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    state = {
 | 
					    state = {
 | 
				
			||||||
        inLogin: false,
 | 
					        inLogin: false,
 | 
				
			||||||
        height: window.innerHeight,
 | 
					        height: window.innerHeight,
 | 
				
			||||||
        width: window.innerWidth
 | 
					        width: window.innerWidth,
 | 
				
			||||||
 | 
					        loginAccount: undefined,
 | 
				
			||||||
 | 
					        totpModalVisible: false,
 | 
				
			||||||
 | 
					        confirmLoading: false
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    componentDidMount() {
 | 
					    componentDidMount() {
 | 
				
			||||||
@ -31,6 +37,15 @@ class LoginForm extends Component {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            let result = await request.post('/login', params);
 | 
					            let result = await request.post('/login', params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (result.code === 0) {
 | 
				
			||||||
 | 
					                // 进行双因子认证
 | 
				
			||||||
 | 
					                this.setState({
 | 
				
			||||||
 | 
					                    loginAccount: params,
 | 
				
			||||||
 | 
					                    totpModalVisible: true
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            if (result.code !== 1) {
 | 
					            if (result.code !== 1) {
 | 
				
			||||||
                throw new Error(result.message);
 | 
					                throw new Error(result.message);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -50,23 +65,54 @@ class LoginForm extends Component {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleOk = async (values) => {
 | 
				
			||||||
 | 
					        this.setState({
 | 
				
			||||||
 | 
					            confirmLoading: true
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        let loginAccount = this.state.loginAccount;
 | 
				
			||||||
 | 
					        loginAccount['totp'] = values['totp'];
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            let result = await request.post('/loginWithTotp', loginAccount);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (result.code !== 1) {
 | 
				
			||||||
 | 
					                throw new Error(result.message);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 跳转登录
 | 
				
			||||||
 | 
					            sessionStorage.removeItem('current');
 | 
				
			||||||
 | 
					            sessionStorage.removeItem('openKeys');
 | 
				
			||||||
 | 
					            localStorage.setItem('X-Auth-Token', result['data']);
 | 
				
			||||||
 | 
					            // this.props.history.push();
 | 
				
			||||||
 | 
					            window.location.href = "/"
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            message.error(e.message);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.setState({
 | 
				
			||||||
 | 
					                confirmLoading: false
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleCancel = () => {
 | 
				
			||||||
 | 
					        this.setState({
 | 
				
			||||||
 | 
					            totpModalVisible: false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <div className='login-bg'
 | 
					            <div className='login-bg'
 | 
				
			||||||
                style={{ width: this.state.width, height: this.state.height, backgroundColor: '#F0F2F5' }}>
 | 
					                 style={{width: this.state.width, height: this.state.height, backgroundColor: '#F0F2F5'}}>
 | 
				
			||||||
                <Card className='login-card' title={null}>
 | 
					                <Card className='login-card' title={null}>
 | 
				
			||||||
                    <div style={{ textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff' }}>
 | 
					                    <div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
 | 
				
			||||||
                        <Title level={1}>Next Terminal</Title>
 | 
					                        <Title level={1}>Next Terminal</Title>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <Form onFinish={this.handleSubmit} className="login-form">
 | 
					                    <Form onFinish={this.handleSubmit} className="login-form">
 | 
				
			||||||
                        <Form.Item name='username' rules={[{ required: true, message: '请输入登录账号!' }]}>
 | 
					                        <Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
 | 
				
			||||||
                            <Input prefix={<UserOutlined />} placeholder="登录账号" />
 | 
					                            <Input prefix={<UserOutlined/>} placeholder="登录账号"/>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item name='password' rules={[{ required: true, message: '请输入登录密码!' }]}>
 | 
					                        <Form.Item name='password' rules={[{required: true, message: '请输入登录密码!'}]}>
 | 
				
			||||||
                            <Input.Password prefix={<LockOutlined />} placeholder="登录密码" />
 | 
					                            <Input.Password prefix={<LockOutlined/>} placeholder="登录密码"/>
 | 
				
			||||||
                        </Form.Item>
 | 
					 | 
				
			||||||
                        <Form.Item name='totp' rules={[]}>
 | 
					 | 
				
			||||||
                            <Input prefix={<OneToOneOutlined />} placeholder="TOTP" />
 | 
					 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item name='remember' valuePropName='checked' initialValue={false}>
 | 
					                        <Form.Item name='remember' valuePropName='checked' initialValue={false}>
 | 
				
			||||||
                            <Checkbox>记住登录</Checkbox>
 | 
					                            <Checkbox>记住登录</Checkbox>
 | 
				
			||||||
@ -79,6 +125,28 @@ class LoginForm extends Component {
 | 
				
			|||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                    </Form>
 | 
					                    </Form>
 | 
				
			||||||
                </Card>
 | 
					                </Card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <Modal title="双因素认证" visible={this.state.totpModalVisible} confirmLoading={this.state.confirmLoading}
 | 
				
			||||||
 | 
					                       onOk={() => {
 | 
				
			||||||
 | 
					                           this.formRef.current
 | 
				
			||||||
 | 
					                               .validateFields()
 | 
				
			||||||
 | 
					                               .then(values => {
 | 
				
			||||||
 | 
					                                   this.formRef.current.resetFields();
 | 
				
			||||||
 | 
					                                   this.handleOk(values);
 | 
				
			||||||
 | 
					                               })
 | 
				
			||||||
 | 
					                               .catch(info => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                               });
 | 
				
			||||||
 | 
					                       }}
 | 
				
			||||||
 | 
					                       onCancel={this.handleCancel}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <Form ref={this.formRef}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <Form.Item name='totp' rules={[{required: true, message: '请输入双因素认证APP中显示的授权码'}]}>
 | 
				
			||||||
 | 
					                            <Input prefix={<OneToOneOutlined/>} placeholder="请输入双因素认证APP中显示的授权码"/>
 | 
				
			||||||
 | 
					                        </Form.Item>
 | 
				
			||||||
 | 
					                    </Form>
 | 
				
			||||||
 | 
					                </Modal>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,12 @@
 | 
				
			|||||||
import React, { Component } from 'react';
 | 
					import React, {Component} from 'react';
 | 
				
			||||||
import { Button, Form, Input, Layout, PageHeader, Image } from "antd";
 | 
					import {Button, Card, Form, Image, Input, Layout, PageHeader} 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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { Content } = Layout;
 | 
					const {Content} = Layout;
 | 
				
			||||||
 | 
					const {Meta} = Card;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes = [
 | 
					const routes = [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@ -19,12 +20,12 @@ const routes = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const formItemLayout = {
 | 
					const formItemLayout = {
 | 
				
			||||||
    labelCol: { span: 3 },
 | 
					    labelCol: {span: 3},
 | 
				
			||||||
    wrapperCol: { span: 6 },
 | 
					    wrapperCol: {span: 6},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
const formTailLayout = {
 | 
					const formTailLayout = {
 | 
				
			||||||
    labelCol: { span: 3 },
 | 
					    labelCol: {span: 3},
 | 
				
			||||||
    wrapperCol: { span: 6, offset: 3 },
 | 
					    wrapperCol: {span: 6, offset: 3},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Info extends Component {
 | 
					class Info extends Component {
 | 
				
			||||||
@ -106,7 +107,7 @@ class Info extends Component {
 | 
				
			|||||||
                        itemRender: itemRender
 | 
					                        itemRender: itemRender
 | 
				
			||||||
                    }}
 | 
					                    }}
 | 
				
			||||||
                    extra={[
 | 
					                    extra={[
 | 
				
			||||||
                        <Logout key='logout' />
 | 
					                        <Logout key='logout'/>
 | 
				
			||||||
                    ]}
 | 
					                    ]}
 | 
				
			||||||
                    subTitle="个人中心"
 | 
					                    subTitle="个人中心"
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
@ -125,7 +126,7 @@ class Info extends Component {
 | 
				
			|||||||
                                },
 | 
					                                },
 | 
				
			||||||
                            ]}
 | 
					                            ]}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <Input type='password' placeholder="请输入原始密码" />
 | 
					                            <Input type='password' placeholder="请输入原始密码"/>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item
 | 
					                        <Form.Item
 | 
				
			||||||
                            {...formItemLayout}
 | 
					                            {...formItemLayout}
 | 
				
			||||||
@ -139,7 +140,7 @@ class Info extends Component {
 | 
				
			|||||||
                            ]}
 | 
					                            ]}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <Input type='password' placeholder="新的密码"
 | 
					                            <Input type='password' placeholder="新的密码"
 | 
				
			||||||
                                onChange={(value) => this.onNewPasswordChange(value)} />
 | 
					                                   onChange={(value) => this.onNewPasswordChange(value)}/>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item
 | 
					                        <Form.Item
 | 
				
			||||||
                            {...formItemLayout}
 | 
					                            {...formItemLayout}
 | 
				
			||||||
@ -155,7 +156,7 @@ class Info extends Component {
 | 
				
			|||||||
                            help={this.state.errorMsg || ''}
 | 
					                            help={this.state.errorMsg || ''}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <Input type='password' placeholder="请和上面输入新的密码保持一致"
 | 
					                            <Input type='password' placeholder="请和上面输入新的密码保持一致"
 | 
				
			||||||
                                onChange={(value) => this.onNewPassword2Change(value)} />
 | 
					                                   onChange={(value) => this.onNewPassword2Change(value)}/>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item {...formTailLayout}>
 | 
					                        <Form.Item {...formTailLayout}>
 | 
				
			||||||
                            <Button type="primary" htmlType="submit">
 | 
					                            <Button type="primary" htmlType="submit">
 | 
				
			||||||
@ -174,11 +175,19 @@ class Info extends Component {
 | 
				
			|||||||
                        </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="使用TOTP应用扫码">
 | 
					                        <Form.Item {...formItemLayout} label="二维码">
 | 
				
			||||||
                            <Image
 | 
					
 | 
				
			||||||
 | 
					                            <Card
 | 
				
			||||||
 | 
					                                hoverable
 | 
				
			||||||
 | 
					                                style={{width: 280}}
 | 
				
			||||||
 | 
					                                cover={<Image
 | 
				
			||||||
 | 
					                                    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>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item
 | 
					                        <Form.Item
 | 
				
			||||||
                            {...formItemLayout}
 | 
					                            {...formItemLayout}
 | 
				
			||||||
@ -186,7 +195,7 @@ class Info extends Component {
 | 
				
			|||||||
                            label="TOTP"
 | 
					                            label="TOTP"
 | 
				
			||||||
                            rules={[]}
 | 
					                            rules={[]}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <Input placeholder="请输入显示的数字" />
 | 
					                            <Input placeholder="请输入双因素认证APP中显示的授权码"/>
 | 
				
			||||||
                        </Form.Item>
 | 
					                        </Form.Item>
 | 
				
			||||||
                        <Form.Item {...formTailLayout}>
 | 
					                        <Form.Item {...formTailLayout}>
 | 
				
			||||||
                            <Button type="primary" htmlType="submit">
 | 
					                            <Button type="primary" htmlType="submit">
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user