优化了双因素认证的流程和页面

This commit is contained in:
dushixiang 2021-01-12 22:36:59 +08:00
parent 3bb7d2d49b
commit 4de18a6a81
5 changed files with 154 additions and 36 deletions

View File

@ -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不匹配")
} }

View File

@ -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)

View File

@ -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;
} }
} }

View File

@ -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>
); );

View File

@ -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">