优化了双因素认证的流程和页面
This commit is contained in:
parent
3bb7d2d49b
commit
4de18a6a81
@ -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,6 +65,40 @@ 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'
|
||||||
@ -65,9 +114,6 @@ class LoginForm extends Component {
|
|||||||
<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>
|
||||||
<Form.Item name='totp' rules={[]}>
|
|
||||||
<Input prefix={<OneToOneOutlined />} placeholder="TOTP" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
|
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
|
||||||
<Checkbox>记住登录</Checkbox>
|
<Checkbox>记住登录</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -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 = [
|
||||||
{
|
{
|
||||||
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user