绑定身份认证器功能完成,使用身份认证器动态密码登录功能完成。

pull/105/head
Apex Liu 2017-11-19 00:09:25 +08:00
parent e73b7c5f6b
commit 980a684d85
23 changed files with 931 additions and 153 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,399 @@
"use strict";
$app.on_init = function (cb_stack) {
$app.dom = {
// title: $('#title'),
// icon_bg: $('#icon-bg'),
tp_time: $('#teleport-time')
, op_message: $('#area-auth [data-field="message"]')
, btn_show_oath_app: $('#btn-show-oath-app')
, dlg_oath_app: $('#dlg-oath-app')
, qrcode: {
area: $('#area-qrcode')
, name: $('#area-qrcode [data-field="name"]')
, image: $('#area-qrcode [data-field="qrcode"]')
, desc: $('#area-qrcode [data-field="desc"]')
}
, auth: {
area: $('#area-auth')
, input_username: $('#area-auth [data-field="input-username"]')
, input_password: $('#area-auth [data-field="input-password"]')
, btn_submit: $('#area-auth [data-field="btn-submit"]')
, message: $('#area-auth [data-field="message"]')
}
, bind: {
dlg: $('#dlg-bind-oath')
, qrcode_img: $('#dlg-bind-oath [data-field="oath-secret-qrcode"]')
, tmp_secret: $('#dlg-bind-oath [data-field="tmp-oath-secret"]')
, input_oath_code: $('#dlg-bind-oath [data-field="oath-code"]')
, btn_submit: $('#dlg-bind-oath [data-field="btn-submit"]')
, message: $('#dlg-bind-oath [data-field="message"]')
}
};
$app.tp_time = 0;
$app.dom.op_message = $app.dom.auth.message;
$app.dom.bind.dlg.on('shown.bs.modal', function () {
$app.dom.op_message = $app.dom.bind.message;
});
$app.dom.bind.dlg.on('hidden.bs.modal', function () {
$app.dom.op_message = $app.dom.auth.message;
});
// $app.show_bind_dlg();
// $app.dom.icon_bg.addClass('fa fa-crosshairs').css('color', '#8140f1');
$app.dom.btn_show_oath_app.click(function () {
$app.dom.dlg_oath_app.modal();
});
$('[data-switch]').click(function () {
var n = $(this).attr('data-switch');
console.log(n);
var name, img, desc;
if (n === 'g-ios-appstore') {
name = '<i class="fa fa-apple"></i> 谷歌身份验证器';
img = 'img/qrcode/google-oath-appstore.png';
desc = '适用于 iOS从 Apple Store 安装';
} else if (n === 'g-android-baidu') {
name = '<i class="fa fa-android"></i> 谷歌身份验证器';
img = 'img/qrcode/google-oath-baidu.png';
desc = '适用于 Android从百度手机助手安装';
} else if (n === 'g-android-google') {
name = '<i class="fa fa-android"></i> 谷歌身份验证器';
img = 'img/qrcode/google-oath-googleplay.png';
desc = '适用于 Android从 Google Play 安装';
} else if (n === 'mi-ios-appstore') {
name = '<i class="fa fa-apple"></i> 小米安全令牌';
img = 'img/qrcode/xiaomi-oath-appstore.png';
desc = '适用于 iOS从 Apple Store 安装';
} else if (n === 'mi-android-mi') {
name = '<i class="fa fa-android"></i> 小米安全令牌';
img = 'img/qrcode/xiaomi-oath-xiaomi.png';
desc = '适用于 Android从小米应用商店安装';
} else if (n === 'wechat') {
name = '<i class="fa fa-wechat"></i> 微信 · 小程序';
img = 'img/qrcode/wechat.png';
desc = '适用于 iOS/Android在微信小程序中搜索“二次验证码”即可';
}
$app.dom.qrcode.name.html(name);
$app.dom.qrcode.image.attr('src', '/static/' + img);
$app.dom.qrcode.desc.html(desc);
if (!$app.dom.qrcode.image.hasClass('selected'))
$app.dom.qrcode.image.addClass('selected');
});
$app.dom.auth.btn_submit.click(function () {
$app.on_auth_user();
});
$app.dom.auth.input_username.keydown(function (event) {
if (event.which === 13) {
$app.dom.auth.input_password.focus();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
$app.dom.auth.input_password.keydown(function (event) {
if (event.which === 13) {
$app.on_auth_user();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
$app.dom.bind.input_oath_code.keydown(function (event) {
if (event.which === 13) {
$app.on_save();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
$app.dom.bind.btn_submit.click(function(){
$app.on_save();
});
// 获取服务器时间
$app.sync_tp_time = function () {
$tp.ajax_post_json('/system/get-time', {},
function (ret) {
if (ret.code === TPE_OK) {
$app.tp_time = tp_utc2local(ret.data);
$app.show_tp_time();
}
},
function () {
}
);
};
$app.sync_tp_time();
$app.show_tp_time = function () {
if ($app.tp_time === 0)
return;
$app.dom.tp_time.text(tp_format_datetime($app.tp_time));
$app.tp_time += 1;
};
setInterval($app.show_tp_time, 1000);
// 每五分钟同步一次服务器时间,避免长时间误差积累导致显示不正确
setInterval($app.sync_tp_time, 1000 * 60 * 5);
cb_stack.exec();
};
$app.hide_op_box = function () {
$app.dom.op_message.hide();
};
$app.show_op_box = function (op_type, op_msg) {
$app.dom.op_message.html(op_msg);
$app.dom.op_message.removeClass().addClass('op_box op_' + op_type);
$app.dom.op_message.show();
};
$app.on_auth_user = function () {
$app.hide_op_box();
var str_username = $app.dom.auth.input_username.val();
var str_password = $app.dom.auth.input_password.val();
if (str_username.length === 0) {
$app.show_op_box('error', '用户名未填写!');
$app.dom.auth.input_username.attr('data-content', "请输入您的用户名!").focus().popover('show');
return;
}
if (str_password.length === 0) {
$app.show_op_box('error', '密码未填写!');
$app.dom.auth.input_password.attr('data-content', "请输入您的密码!").focus().popover('show');
return;
}
$app.dom.auth.btn_submit.attr('disabled', 'disabled');
$tp.ajax_post_json('/user/verify-user', {username: str_username, password: str_password},
function (ret) {
$app.dom.auth.btn_submit.removeAttr('disabled');
if (ret.code === TPE_OK) {
// 验证成功
$app.hide_op_box();
// 显示绑定对话框
$app.show_bind_dlg();
}
else {
$app.hide_op_box();
$app.show_op_box('error', tp_error_msg(ret.code, ret.message));
}
},
function () {
$app.hide_op_box();
$app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
$app.dom.auth.btn_submit.removeAttr('disabled');
}
);
};
$app.show_bind_dlg = function () {
var str_username = $app.dom.auth.input_username.val();
$tp.ajax_post_json('/user/gen-oath-secret', {},
function (ret) {
if (ret.code === TPE_OK) {
$app.dom.bind.qrcode_img.attr('src', '/user/oath-secret-qrcode?u=' + str_username + '&rnd=' + Math.random());
$app.dom.bind.tmp_secret.text(ret.data.tmp_oath_secret);
$app.dom.bind.dlg.modal({backdrop: 'static'});
} else {
$tp.notify_error('无法绑定身份验证器:' + tp_error_msg(ret.code, ret.message));
}
},
function () {
$tp.notify_error('网路故障,无法连接到服务器!');
}
);
};
$app.on_save = function () {
var str_username = $app.dom.auth.input_username.val();
var str_password = $app.dom.auth.input_password.val();
var oath_code = $app.dom.bind.input_oath_code.val();
if (oath_code.length === 0) {
$app.show_op_box('error', '动态验证码未填写!');
$app.dom.bind.input_oath_code.attr('data-content', "请输入动态验证码!").focus().popover('show');
return;
}
if (oath_code.length !== 6) {
$app.show_op_box('error', '动态验证码错误!');
$app.dom.bind.input_oath_code.attr('data-content', "动态验证码为 6 位数字!").focus().popover('show');
return;
}
$tp.ajax_post_json('/user/do-bind-oath', {username: str_username, password: str_password, oath_code: oath_code},
function (ret) {
$app.dom.auth.btn_submit.removeAttr('disabled');
if (ret.code === TPE_OK) {
// 验证成功
$app.hide_op_box();
$app.show_op_box('success', '绑定成功,正在转到登录界面!');
setTimeout(function(){
window.location.href = '/';
}, 3000);
}
else {
$app.hide_op_box();
$app.show_op_box('error', tp_error_msg(ret.code, ret.message));
}
},
function () {
$app.hide_op_box();
$app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
$app.dom.auth.btn_submit.removeAttr('disabled');
}
);
};
// $app.on_send_find_password_email = function () {
// $app.hide_op_box();
// var str_username = $app.dom.find.input_username.val();
// var str_email = $app.dom.find.input_email.val();
// var str_captcha = $app.dom.find.input_captcha.val();
//
// if (str_username.length === 0) {
// $app.show_op_box('error', '账号未填写!');
// $app.dom.find.input_username.attr('data-content', "请填写您的账号!").focus().popover('show');
// return;
// }
//
// if (str_email.length === 0) {
// $app.show_op_box('error', '电子邮件地址未填写!');
// $app.dom.find.input_email.attr('data-content', "请填写您的电子邮件地址!").focus().popover('show');
// return;
// }
//
// if (!tp_check_email(str_email)) {
// $app.show_op_box('error', '无效的电子邮件地址!');
// $app.dom.find.input_email.attr('data-content', "请检查输入的电子邮件地址!").focus().popover('show');
// return;
// }
//
// if (str_captcha.length !== 4) {
// $app.show_op_box('error', '验证码错误!');
// $app.dom.find.input_captcha.attr('data-content', "验证码为4位数字和字母的组合请重新填写").focus().select().popover('show');
// return;
// }
//
// $app.dom.find.btn_submit.attr('disabled', 'disabled');
// $tp.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha},
// function (ret) {
// if (ret.code === TPE_OK) {
// // 验证成功
// $app.hide_op_box();
// $app.show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在发送密码重置确认函,请稍候...');
// $app.do_send_reset_email(str_username, str_email, str_captcha);
// }
// else {
// $app.dom.find.btn_submit.removeAttr('disabled');
// $app.hide_op_box();
// $app.show_op_box('error', tp_error_msg(ret.code, ret.message));
// $app.dom.captcha_image.attr('src', '/auth/captcha?h=28&rnd=' + Math.random());
// $app.dom.input_captcha.focus().select().val('');
// }
// },
// function () {
// $app.hide_op_box();
// $app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
// $app.dom.find.btn_submit.removeAttr('disabled');
// }
// );
// };
//
// $app.do_send_reset_email = function (str_username, str_email, str_captcha) {
// $tp.ajax_post_json('/user/do-reset-password', {
// mode: 3,
// username: str_username,
// email: str_email,
// captcha: str_captcha
// },
// function (ret) {
// if (ret.code === TPE_OK) {
// $app.dom.find.btn_submit.slideUp('fast');
// $app.show_op_box('success', '密码重置确认函已发送,请注意查收!');
// } else {
// $app.dom.find.btn_submit.removeAttr('disabled');
// $app.hide_op_box();
// var msg = '';
// if (ret.code === TPE_NOT_EXISTS)
// msg = tp_error_msg(ret.code, '用户不存在,请检查输入的用户和电子邮件地址是否匹配!');
// else
// msg = tp_error_msg(ret.code, ret.message);
// $app.show_op_box('error', msg);
// }
// },
// function () {
// $app.dom.find.btn_submit.removeAttr('disabled');
// $app.hide_op_box();
// $app.show_op_box('error', '网络故障,密码重置确认函发送失败!');
// },
// 15000
// );
// };
//
// $app.on_set_new_password = function () {
// $app.hide_op_box();
// var str_password = $app.dom.set_password.input_password.val();
//
// if (str_password.length === 0) {
// $app.show_op_box('error', '密码未填写!');
// $app.dom.set_password.input_password.attr('data-content', "请设置您的新密码!").focus().popover('show');
// return;
// }
//
// if ($app.options.force_strong) {
// if (!tp_check_strong_password(str_password)) {
// $app.show_op_box('error', tp_error_msg(TPE_FAILED, '抱歉,不能使用弱密码!'));
// $app.dom.set_password.input_password.attr('data-content', "请设置强密码至少8位必须包含大写字母、小写字母以及数字").focus().popover('show');
// return;
// }
// }
//
// $tp.ajax_post_json('/user/do-reset-password', {
// mode: 4,
// token: $app.options.token,
// password: str_password
// },
// function (ret) {
// $app.dom.find.btn_submit.removeAttr('disabled');
// if (ret.code === TPE_OK) {
// $app.show_op_box('success', '密码已重置,正在转到登录界面!');
// setTimeout(function () {
// window.location.href = '/';
// }, 2000);
// } else {
// var msg = '';
// if (ret.code === TPE_NOT_EXISTS)
// msg = tp_error_msg(ret.code, '无效的密码重置链接!');
// else
// msg = tp_error_msg(ret.code, ret.message);
// $app.show_op_box('error', msg);
// }
// },
// function () {
// $app.dom.find.btn_submit.removeAttr('disabled');
// $app.hide_op_box();
// $app.show_op_box('error', '网络故障,密码重置失败!');
// }
// );
// };

View File

@ -114,14 +114,14 @@ $app.on_init = function (cb_stack, cb_args) {
});
$app.dom.btn_reset_oath_code.click(function () {
$tp.ajax_post_json('/auth/oath-secret-reset', {},
$tp.ajax_post_json('/user/gen-oath-secret', {},
function (ret) {
if (ret.code === TPE_OK) {
$app.dom.oath_secret_image.attr('src', '/auth/oath-secret-qrcode?' + Math.random());
$app.dom.oath_secret_image.attr('src', '/user/oath-secret-qrcode?' + Math.random());
$app.dom.tmp_oath_secret.text(ret.data.tmp_oath_secret);
$app.dom.dlg_reset_oath_code.modal({backdrop: 'static'});
} else {
$tp.notify_error('发生内部错误!' + tp_error_msg(ret.code, ret.message));
$tp.notify_error('无法绑定身份验证器:' + tp_error_msg(ret.code, ret.message));
}
},
function () {

View File

@ -46,6 +46,7 @@ $app.on_init = function (cb_stack) {
if (event.which === 13) {
$app.dom.find.input_email.focus();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
@ -53,6 +54,7 @@ $app.on_init = function (cb_stack) {
if (event.which === 13) {
$app.dom.find.input_captcha.focus();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
@ -60,6 +62,7 @@ $app.on_init = function (cb_stack) {
if (event.which === 13) {
$app.on_send_find_password_email();
} else {
$app.hide_op_box();
$('[data-toggle="popover"]').popover('hide');
}
});
@ -145,16 +148,20 @@ $app.on_send_find_password_email = function () {
var str_captcha = $app.dom.find.input_captcha.val();
if (str_username.length === 0) {
$app.show_op_box('error', '账号未填写!');
$app.dom.find.input_username.attr('data-content', "请填写您的账号!").focus().popover('show');
// $app.dom.find.input_username.focus();
$app.show_op_box('error', '用户名未填写!');
$app.dom.find.input_username.attr('data-content', "请填写您的用户名!").focus().popover('show');
return;
}
if (str_email.length === 0) {
$app.show_op_box('error', '邮箱未填写!');
$app.dom.find.input_email.attr('data-content', "请填写您的邮箱!").focus().popover('show');
// $app.dom.find.input_email.focus();
$app.show_op_box('error', '电子邮件地址未填写!');
$app.dom.find.input_email.attr('data-content', "请填写您的电子邮件地址!").focus().popover('show');
return;
}
if (!tp_check_email(str_email)) {
$app.show_op_box('error', '无效的电子邮件地址!');
$app.dom.find.input_email.attr('data-content', "请检查输入的电子邮件地址!").focus().popover('show');
return;
}
@ -205,7 +212,7 @@ $app.do_send_reset_email = function (str_username, str_email, str_captcha) {
$app.hide_op_box();
var msg = '';
if (ret.code === TPE_NOT_EXISTS)
msg = tp_error_msg(ret.code, '没有此用户');
msg = tp_error_msg(ret.code, '用户不存在,请检查输入的用户和电子邮件地址是否匹配!');
else
msg = tp_error_msg(ret.code, ret.message);
$app.show_op_box('error', msg);

View File

@ -914,8 +914,6 @@ $app.create_dlg_reset_password = function () {
var dlg = {};
dlg.dom_id = 'dlg-reset-password';
dlg.field_id = -1;
// dlg.field_username = '';
// dlg.field_surname = '';
dlg.field_email = '';
dlg.field_password = '';
@ -1025,7 +1023,6 @@ $app.create_dlg_reset_password = function () {
function (ret) {
if (ret.code === TPE_OK) {
$tp.notify_success('用户密码重置成功!');
// $app.table_users.load_data();
dlg.dom.dialog.modal('hide');
} else {
$tp.notify_error('用户密码重置失败:' + tp_error_msg(ret.code, ret.message));

View File

@ -77,14 +77,29 @@ label {
font-family: @font-family-mono;
}
.important {
//font-weight:bold;
color: #d45f1c;
//font-style: italic;
}
hr.hr-sm {
margin-top: 5px;
margin-bottom: 5px;
}
//==============================================
// 重载bootstrap的样式
//==============================================
ul {
list-style: none;
}
ul.list {
//list-style: none;
margin: 0;
padding: 3px 0 5px 0;
li {
margin: 3px 3px 5px 10px;
}
}
.btn-single-line {
white-space: nowrap;

View File

@ -145,6 +145,11 @@
}
}
.modal-dialog .modal-content .modal-header {
//padding: 10px;
background-color: #f7f7f7;
}
//.alert-sm {
// padding: 5px;
// margin-bottom: 10px;

View File

@ -202,7 +202,7 @@ body {
position: absolute;
min-height: @error-icon-size+30px;
overflow: hidden;
.fa {
& > .fa {
margin-top: 30px;
font-size: @error-icon-size;
//color: rgb(255, 101, 0);
@ -227,7 +227,7 @@ body {
hr {
//border-top: 1px solid #d3d3d3;
//border-bottom: 1px solid #fff;
margin: 10px 0 20px;
//margin: 10px 0 20px;
border-top: none;
border-bottom: 1px dashed #d3d3d3;
}

View File

@ -61,6 +61,7 @@
<span class="input-group-addon"><i class="fa fa-clock-o fa-fw"></i></span>
<input id="oath-code" type="text" class="form-control" placeholder="6位数字身份验证器动态验证码" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
<p class="input-addon-desc"><a href="/user/bind-oath">尚未绑定身份验证器?</a></p>
</div>
</div>

View File

@ -0,0 +1,219 @@
<%!
page_title_ = '绑定身份验证器'
%>
<%inherit file="../page_single_base.mako"/>
<%block name="extend_js_file">
<script type="text/javascript" src="${ static_url('js/user/bind-oath.js') }"></script>
</%block>
<%block name="embed_css">
<style type="text/css">
.step-name {
font-size: 18px;
## font-weight:bold;
color: #e55a1d;
}
.input-addon-desc {
text-align: right;
font-size: 90%;
color: #707070;
}
.time-box {
display: inline-block;
margin: 10px;
padding: 10px;
border: 1px solid #bcffbd;
background-color: #d0ffcd;
border-radius: 5px;
color: #646464;
}
.tp-time {
font-weight: bold;
font-size: 16px;
color: #063c06;
}
#area-qrcode {
margin: 10px auto 0;
text-align: center;
}
#area-qrcode .qrcode-name {
font-size: 18px;
color: #5483ff;
}
img.qrcode-img {
margin: 5px;
padding: 10px;
}
img.qrcode-img.selected {
border: 1px solid #bdbdbd;
}
.oath-code {
font-family: 'Courier New', Consolas, Lucida Console, Monaco, Courier, monospace;
font-size: 26px;
line-height: 26px;
font-weight: bold;
color: #559f47;
}
</style>
</%block>
<%block name="page_header">
<div class="container-fluid top-navbar">
<div class="brand"><a href="/"><span class="site-logo"></span></a></div>
<div class="breadcrumb-container">
<ol class="breadcrumb">
<li><i class="fa fa-key"></i> 绑定身份验证器</li>
</ol>
</div>
</div>
</%block>
<div class="page-content">
<div class="info-box">
<div class="info-icon-box">
<i class="fa fa-shield" style="color:#8140f1;"></i>
</div>
<div class="info-message-box">
<div class="title">绑定身份验证器</div>
<hr/>
<div id="content" class="content">
<p class="step-name">第一步:安装身份验证器</p>
<p>请在你的手机上安装身份验证器App。<a href="javascript:;" id="btn-show-oath-app">点击此处获取安装方式</a></p>
<hr/>
<p class="step-name">第二步:检查服务器时间</p>
<p>请注意检查您的手机时间与teleport服务器时间是否同步如果两者<span class="important">时间偏差超过两分钟则无法绑定</span>,请及时通知系统管理员处理!</p>
<div class="time-box"><i class="fa fa-clock-o"></i> TELEPORT服务器时间<span class="tp-time mono" id="teleport-time">-</span></div>
<hr/>
<p class="step-name">第三步:认证并绑定</p>
<div class="row" style="padding:0 20px;">
<div id="area-auth">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user-circle-o fa-fw"></i></span>
<input data-field="input-username" type="text" class="form-control mono" placeholder="teleport系统用户名" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
<div class="input-group" style="margin-top:10px;">
<span class="input-group-addon"><i class="fa fa-key fa-fw"></i></span>
<input data-field="input-password" type="password" class="form-control mono" placeholder="请输入密码" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
<div style="margin:20px 0;">
<button type="button" class="btn btn-primary" data-field="btn-submit" style="width:100%;"><i class="fa fa-check fa-fw"></i> 用户身份认证</button>
<div data-field="message" class="alert alert-danger" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%block name="extend_content">
<div class="modal fade" id="dlg-oath-app" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title"><i class="fa fa-shield"></i> 身份验证器</h3>
</div>
<div class="modal-body">
<p>选择您喜欢的身份验证器进行安装:</p>
<div class="row">
<div class="col-md-4">
<ul class="list">
<li>谷歌身份验证器
<ul class="list">
<li><a href="javascript:;" data-switch="g-ios-appstore"><i class="fa fa-apple fa-fw"></i> iOSApple Store</a></li>
<li><a href="javascript:;" data-switch="g-android-baidu"><i class="fa fa-android fa-fw"></i> Android百度手机助手</a></li>
<li><a href="javascript:;" data-switch="g-android-google"><i class="fa fa-android fa-fw"></i> AndroidGoogle Play</a></li>
</ul>
</li>
<li>小米安全令牌
<ul class="list">
<li><a href="javascript:;" data-switch="mi-ios-appstore"><i class="fa fa-apple fa-fw"></i> iOSApple Store</a></li>
<li><a href="javascript:;" data-switch="mi-android-mi"><i class="fa fa-android fa-fw"></i> Android小米应用商店</a></li>
</ul>
</li>
<li>微信小程序
<ul class="list">
<li><a href="javascript:;" data-switch="wechat"><i class="fa fa-wechat fa-fw"></i> 二次验证码</a> <span class="label label-success">推荐</span></li>
</ul>
</li>
</ul>
</div>
<div class="col-md-8">
<div id="area-qrcode">
<p data-field="name" class="qrcode-name"></p>
<img class="qrcode-img" data-field="qrcode" src="${ static_url('img/qrcode/select-oath-app.png') }">
<p data-field="desc"></p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal"><i class="fa fa-close fa-fw"></i> 关闭</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlg-bind-oath" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">绑定身份验证器</h3>
</div>
<div class="modal-body">
<p>请在手机上打开身份验证器,点击增加账号按钮,然后选择“扫描条形码”并扫描下面的二维码来完成账号绑定。</p>
<p style="text-align: center;"><img data-field="oath-secret-qrcode" src="" style="border:1px solid #b7b7b7;"></p>
<p>如果无法扫描二维码,则可以选择“手动输入验证码”,设置一个容易记忆的账号名称,并确保“基于时间”一项是选中的,然后在“密钥”一项中输入下列密钥:</p>
<div style="text-align:center;" class="oath-code"><span data-field="tmp-oath-secret"></span></div>
<hr/>
<p>然后请在下面的动态验证码输入框中输入身份验证器提供的6位数字</p>
<div class="row">
<div class="col-sm-4" style="text-align:right;">
<span style="line-height:34px;">动态验证码:</span>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" data-field="oath-code" data-toggle="popover" data-trigger="manual" data-placement="top">
</div>
</div>
<div data-field="message" class="alert alert-danger" style="display:none;margin-top:10px;">aabb</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary" data-field="btn-submit"><i class="fa fa-check fa-fw"></i> 验证并完成绑定</button>
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal"><i class="fa fa-close fa-fw"></i> 取消</button>
</div>
</div>
</div>
</div>
</%block>
<%block name="embed_js">
<script type="text/javascript">
"use strict";
## $app.add_options(${page_param});
## console.log($app.options);
</script>
</%block>

View File

@ -31,7 +31,7 @@
<ul class="nav nav-tabs">
<li class="active"><a href="#info" data-toggle="tab">个人信息</a></li>
<li><a href="#password" data-toggle="tab">修改密码</a></li>
<li><a href="#oath" data-toggle="tab">身份证器</a></li>
<li><a href="#oath" data-toggle="tab">身份证器</a></li>
</ul>
<!-- Tab panes -->

View File

@ -23,7 +23,7 @@
<%block name="page_header">
<div class="container-fluid top-navbar">
<div class="brand"><a href="/" target="_blank"><span class="site-logo"></span></a></div>
<div class="brand"><a href="/"><span class="site-logo"></span></a></div>
<div class="breadcrumb-container">
<ol class="breadcrumb">
<li><i class="fa fa-key"></i> 密码管理</li>

View File

@ -83,10 +83,20 @@ controllers = [
(r'/user/update-users', user.DoUpdateUsersHandler),
# - [json] 获取用户列表
(r'/user/get-users', user.DoGetUsersHandler),
# - 用户重设密码页面 /auth/reset-password
(r'/user/reset-password', user.ResetPasswordHandler),
# - [json] 重置密码
(r'/user/do-reset-password', user.DoResetPasswordHandler),
# - 用户重设密码页面 /auth/reset-password?token=D3672DFF256B6B6F37AF8A922D7D83B4
(r'/user/reset-password', user.ResetPasswordHandler),
# - 用户绑定OATH
(r'/user/bind-oath', user.BindOathHandler),
# - [json] 用户绑定OATH
(r'/user/gen-oath-secret', user.DoGenerateOathSecretHandler),
# - 显示OATH密钥二维码
(r'/user/oath-secret-qrcode', user.OathSecretQrCodeHandler),
# - [json] 获取用户信息
(r'/user/verify-user', user.DoVerifyUserHandler),
# - [json] 绑定身份认证器
(r'/user/do-bind-oath', user.DoBindOathHandler),
# - 用户组管理页面
(r'/user/group', user.GroupListHandler),

View File

@ -2,16 +2,15 @@
import json
from app.const import *
from app.base.configs import get_cfg
from app.base.controller import TPBaseHandler, TPBaseJsonHandler
from app.base.logger import log
from app.logic.auth.oath import tp_oath_generate_secret, tp_oath_generate_qrcode, tp_oath_verify_code
from app.logic.auth.captcha import tp_captcha_generate_image
from app.model import user
from app.model import syslog
from app.logic.auth.password import tp_password_verify
from app.base.utils import tp_timestamp_utc_now
from app.const import *
from app.logic.auth.captcha import tp_captcha_generate_image
from app.logic.auth.oath import tp_oath_verify_code
from app.logic.auth.password import tp_password_verify
from app.model import syslog
from app.model import user
class LoginHandler(TPBaseHandler):
@ -98,58 +97,75 @@ class DoLoginHandler(TPBaseJsonHandler):
if len(username) == 0:
return self.write_json(TPE_PARAM, '未提供登录用户名')
err, user_info = user.get_by_username(username)
if login_type not in [TP_LOGIN_AUTH_USERNAME_PASSWORD,
TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA,
TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH
]:
password = None
if login_type not in [TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH,
TP_LOGIN_AUTH_USERNAME_OATH
]:
oath = None
err, user_info = user.login(self, username, password=password, oath_code=oath)
if err != TPE_OK:
if err == TPE_NOT_EXISTS:
err = TPE_USER_AUTH
syslog.sys_log({'username': username, 'surname': username}, self.request.remote_ip, TPE_NOT_EXISTS, '登录失败,用户`{}`不存在'.format(username))
return self.write_json(err)
if user_info.privilege == 0:
# 尚未为此用户设置角色
return self.write_json(TPE_PRIVILEGE, '用户尚未分配角色')
if user_info['state'] == TP_STATE_LOCKED:
# 用户已经被锁定,如果系统配置为一定时间后自动解锁,则更新一下用户信息
if sys_cfg.login.lock_timeout != 0:
if tp_timestamp_utc_now() - user_info.lock_time > sys_cfg.login.lock_timeout * 60:
user_info.fail_count = 0
user_info.state = TP_STATE_NORMAL
if user_info['state'] == TP_STATE_LOCKED:
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_LOCKED, '登录失败,用户已被锁定')
return self.write_json(TPE_USER_LOCKED)
elif user_info['state'] == TP_STATE_DISABLED:
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_DISABLED, '登录失败,用户已被禁用')
return self.write_json(TPE_USER_DISABLED)
elif user_info['state'] != TP_STATE_NORMAL:
syslog.sys_log(user_info, self.request.remote_ip, TPE_FAILED, '登录失败,系统内部错误')
return self.write_json(TPE_FAILED)
err_msg = ''
if login_type in [TP_LOGIN_AUTH_USERNAME_PASSWORD, TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
# 如果系统配置了密码有效期,则检查用户的密码是否失效
if sys_cfg.password.timeout != 0:
pass
if not tp_password_verify(password, user_info['password']):
err, is_locked = user.update_fail_count(self, user_info)
if is_locked:
err_msg = '用户被临时锁定!'
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_AUTH, '登录失败,密码错误!{}'.format(err_msg))
return self.write_json(TPE_USER_AUTH)
if login_type in [TP_LOGIN_AUTH_USERNAME_OATH, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
# use oath
if not tp_oath_verify_code(user_info['oath_secret'], oath):
err, is_locked = user.update_fail_count(self, user_info)
if is_locked:
err_msg = '用户被临时锁定!'
syslog.sys_log(user_info, self.request.remote_ip, TPE_OATH_MISMATCH, "登录失败,身份验证器动态验证码错误!{}".format(err_msg))
return self.write_json(TPE_OATH_MISMATCH)
# err, user_info = user.get_by_username(username)
# if err != TPE_OK:
# if err == TPE_NOT_EXISTS:
# syslog.sys_log({'username': username, 'surname': username}, self.request.remote_ip, TPE_NOT_EXISTS, '登录失败,用户`{}`不存在'.format(username))
# return self.write_json(err)
#
# if user_info.privilege == 0:
# # 尚未为此用户设置角色
# return self.write_json(TPE_PRIVILEGE, '用户尚未分配角色')
#
# if user_info['state'] == TP_STATE_LOCKED:
# # 用户已经被锁定,如果系统配置为一定时间后自动解锁,则更新一下用户信息
# if sys_cfg.login.lock_timeout != 0:
# if tp_timestamp_utc_now() - user_info.lock_time > sys_cfg.login.lock_timeout * 60:
# user_info.fail_count = 0
# user_info.state = TP_STATE_NORMAL
# if user_info['state'] == TP_STATE_LOCKED:
# syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_LOCKED, '登录失败,用户已被锁定')
# return self.write_json(TPE_USER_LOCKED)
# elif user_info['state'] == TP_STATE_DISABLED:
# syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_DISABLED, '登录失败,用户已被禁用')
# return self.write_json(TPE_USER_DISABLED)
# elif user_info['state'] != TP_STATE_NORMAL:
# syslog.sys_log(user_info, self.request.remote_ip, TPE_FAILED, '登录失败,系统内部错误')
# return self.write_json(TPE_FAILED)
#
# err_msg = ''
# if login_type in [TP_LOGIN_AUTH_USERNAME_PASSWORD, TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
# # 如果系统配置了密码有效期,则检查用户的密码是否失效
# if sys_cfg.password.timeout != 0:
# pass
#
# if not tp_password_verify(password, user_info['password']):
# err, is_locked = user.update_fail_count(self, user_info)
# if is_locked:
# err_msg = '用户被临时锁定!'
# syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_AUTH, '登录失败,密码错误!{}'.format(err_msg))
# return self.write_json(TPE_USER_AUTH)
#
# if login_type in [TP_LOGIN_AUTH_USERNAME_OATH, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
# # use oath
# if not tp_oath_verify_code(user_info['oath_secret'], oath):
# err, is_locked = user.update_fail_count(self, user_info)
# if is_locked:
# err_msg = '用户被临时锁定!'
# syslog.sys_log(user_info, self.request.remote_ip, TPE_OATH_MISMATCH, "登录失败,身份验证器动态验证码错误!{}".format(err_msg))
# return self.write_json(TPE_OATH_MISMATCH)
self._user = user_info
self._user['_is_login'] = True
del self._user['password']
del self._user['oath_secret']
# del self._user['password']
# del self._user['oath_secret']
if remember:
self.set_session('user', self._user, 12 * 60 * 60)
@ -185,7 +201,7 @@ class CaptchaHandler(TPBaseHandler):
def get(self):
h = int(self.get_argument('h', 36))
code, img_data = tp_captcha_generate_image(h)
self.set_session('captcha', code)
self.set_session('captcha', code, expire=10 * 60) # 验证码有效期为10分钟
self.set_header('Content-Type', 'image/jpeg')
self.write(img_data)
@ -214,7 +230,6 @@ class VerifyCaptchaHandler(TPBaseJsonHandler):
return self.write_json(TPE_OK)
# class ModifyPwd(TPBaseUserAuthJsonHandler):
# def post(self):
# args = self.get_argument('args', None)

View File

@ -1,21 +1,25 @@
# -*- coding: utf-8 -*-
import os
import json
import time
import csv
import json
import os
import time
from app.const import *
from app.base.configs import get_cfg
from app.base.session import session_manager
from app.base import mail
from app.model import user
from app.model import group
from app.logic.auth.password import tp_password_generate_secret
from app.base.utils import tp_check_strong_password
import tornado.gen
from app.base.logger import *
from app.base import mail
from app.base.configs import get_cfg
from app.base.controller import TPBaseHandler, TPBaseJsonHandler
from app.base.logger import *
from app.base.session import session_manager
from app.base.utils import tp_check_strong_password
from app.base.utils import tp_timestamp_utc_now
from app.logic.auth.oath import tp_oath_verify_code
from app.const import *
from app.logic.auth.oath import tp_oath_generate_secret, tp_oath_generate_qrcode
from app.logic.auth.password import tp_password_generate_secret, tp_password_verify
from app.model import group
from app.model import syslog
from app.model import user
class UserListHandler(TPBaseHandler):
@ -110,6 +114,98 @@ class ResetPasswordHandler(TPBaseHandler):
self.render('user/reset-password.mako', page_param=json.dumps(param))
class BindOathHandler(TPBaseHandler):
def get(self):
self.render('user/bind-oath.mako')
class DoGenerateOathSecretHandler(TPBaseJsonHandler):
def post(self):
oath_secret = tp_oath_generate_secret()
self.set_session('tmp_oath_secret', oath_secret)
return self.write_json(TPE_OK, data={"tmp_oath_secret": oath_secret})
class DoVerifyUserHandler(TPBaseJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is None:
return self.write_json(TPE_PARAM)
try:
args = json.loads(args)
except:
return self.write_json(TPE_JSON_FORMAT)
try:
username = args['username']
password = args['password']
except:
return self.write_json(TPE_PARAM)
err, user_info = user.login(self, username, password=password)
if err != TPE_OK:
if err == TPE_NOT_EXISTS:
err = TPE_USER_AUTH
return self.write_json(err)
return self.write_json(TPE_OK)
class DoBindOathHandler(TPBaseJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is None:
return self.write_json(TPE_PARAM)
try:
args = json.loads(args)
except:
return self.write_json(TPE_JSON_FORMAT)
try:
username = args['username']
password = args['password']
oath_code = args['oath_code']
except:
return self.write_json(TPE_PARAM)
err, user_info = user.login(self, username, password=password)
if err != TPE_OK:
if err == TPE_NOT_EXISTS:
err = TPE_USER_AUTH
return self.write_json(err)
secret = self.get_session('tmp_oath_secret', None)
if secret is None:
return self.write_json(TPE_FAILED, '内部错误!')
self.del_session('tmp_oath_secret')
if not tp_oath_verify_code(secret, oath_code):
return self.write_json(TPE_OATH_MISMATCH)
err = user.update_oath_secret(user_info['id'], secret)
if err != TPE_OK:
return self.write_json(err)
return self.write_json(TPE_OK)
class OathSecretQrCodeHandler(TPBaseHandler):
def get(self):
username = self.get_argument('u', None)
if username is None:
user_info = self.get_current_user()
username = user_info['username']
username = username + '@teleport'
secret = self.get_session('tmp_oath_secret', None)
img_data = tp_oath_generate_qrcode(username, secret)
self.set_header('Content-Type', 'image/jpeg')
self.write(img_data)
class DoGetUserInfoHandler(TPBaseJsonHandler):
def post(self, user_id):
ret = self.check_privilege(TP_PRIVILEGE_USER_CREATE | TP_PRIVILEGE_USER_DELETE | TP_PRIVILEGE_USER_LOCK | TP_PRIVILEGE_USER_GROUP)

View File

@ -19,43 +19,6 @@ from wheezy.captcha.image import warp
_captcha_chars = 'AaCDdEeFfHJjKkLMmNnPpQRTtVvWwXxYy34679'
def __tp_captcha_generate_image():
captcha_image_t = captcha(
width=136,
height=36,
drawings=[
background(color='#eeeeee'),
# curve(color='#4388d5', width=1, number=10),
curve(color='#4388d5', width=1, number=10),
curve(color='#af6fff', width=3, number=16),
noise(number=80, color='#eeeeee', level=3),
text(fonts=[
os.path.join(get_cfg().res_path, 'fonts', '001.ttf')
],
# font_sizes=(28, 34, 36, 32),
font_sizes=(34, 40, 32, 36),
color='#63a8f5',
# squeeze_factor=1.2,
squeeze_factor=0.9,
drawings=[
# warp(dx_factor=0.05, dy_factor=0.05),
warp(dx_factor=0.03, dy_factor=0.03),
rotate(angle=20),
offset()
]),
# curve(color='#af6fff', width=3, number=16),
noise(number=30, color='#eeeeee', level=2),
smooth(),
])
chars_t = random.sample(_captcha_chars, 4)
image = captcha_image_t(chars_t)
out = io.BytesIO()
image.save(out, "jpeg", quality=100)
return ''.join(chars_t), out.getvalue()
def tp_captcha_generate_image(h):
if h >= 32:
captcha_image_t = captcha(
@ -67,20 +30,6 @@ def tp_captcha_generate_image(h):
curve(color='#4388d5', width=1, number=10),
curve(color='#af6fff', width=3, number=16),
noise(number=80, color='#eeeeee', level=3),
# text(fonts=[
# os.path.join(get_cfg().res_path, 'fonts', '001.ttf')
# ],
# # font_sizes=(28, 34, 36, 32),
# font_sizes=(24, 20, 22, 26),
# color='#cecece',
# # squeeze_factor=1.2,
# squeeze_factor=0.9,
# drawings=[
# # warp(dx_factor=0.05, dy_factor=0.05),
# warp(dx_factor=0.03, dy_factor=0.03),
# rotate(angle=20),
# offset()
# ]),
smooth(),
text(fonts=[
os.path.join(get_cfg().res_path, 'fonts', '001.ttf')

View File

@ -8,6 +8,8 @@ from app.base.logger import log
from app.base.utils import tp_timestamp_utc_now, tp_generate_random
from app.const import *
from app.model import syslog
from app.logic.auth.password import tp_password_verify
from app.logic.auth.oath import tp_oath_verify_code
def get_user_info(user_id):
@ -46,6 +48,67 @@ def get_by_username(username):
return TPE_OK, s.recorder[0]
def login(handler, username, password=None, oath_code=None):
sys_cfg = get_cfg().sys
err, user_info = get_by_username(username)
if err != TPE_OK:
# if err == TPE_NOT_EXISTS:
# syslog.sys_log({'username': username, 'surname': username}, handler.request.remote_ip, TPE_NOT_EXISTS, '用户身份验证失败,用户`{}`不存在'.format(username))
return err, None
print(user_info)
if user_info.privilege == 0:
# 尚未为此用户设置角色
return TPE_PRIVILEGE, None
if user_info['state'] == TP_STATE_LOCKED:
# 用户已经被锁定,如果系统配置为一定时间后自动解锁,则更新一下用户信息
if sys_cfg.login.lock_timeout != 0:
if tp_timestamp_utc_now() - user_info.lock_time > sys_cfg.login.lock_timeout * 60:
user_info.fail_count = 0
user_info.state = TP_STATE_NORMAL
if user_info['state'] == TP_STATE_LOCKED:
syslog.sys_log(user_info, handler.request.remote_ip, TPE_USER_LOCKED, '用户已被临时锁定')
return TPE_USER_LOCKED, None
elif user_info['state'] == TP_STATE_DISABLED:
syslog.sys_log(user_info, handler.request.remote_ip, TPE_USER_DISABLED, '用户已被禁用')
return TPE_USER_DISABLED, None
elif user_info['state'] != TP_STATE_NORMAL:
syslog.sys_log(user_info, handler.request.remote_ip, TPE_FAILED, '用户身份验证失败,系统内部错误')
return TPE_FAILED, None
err_msg = ''
if password is not None:
# 如果系统配置了密码有效期,则检查用户的密码是否失效
if sys_cfg.password.timeout != 0:
pass
if not tp_password_verify(password, user_info['password']):
err, is_locked = update_fail_count(handler, user_info)
if is_locked:
err_msg = '用户被临时锁定!'
syslog.sys_log(user_info, handler.request.remote_ip, TPE_USER_AUTH, '登录失败,密码错误!{}'.format(err_msg))
return TPE_USER_AUTH, None
if oath_code is not None:
# use oath
if len(user_info['oath_secret']) == 0:
return TPE_OATH_MISMATCH, None
if not tp_oath_verify_code(user_info['oath_secret'], oath_code):
err, is_locked = update_fail_count(handler, user_info)
if is_locked:
err_msg = '用户被临时锁定!'
syslog.sys_log(user_info, handler.request.remote_ip, TPE_OATH_MISMATCH, "登录失败,身份验证器动态验证码错误!{}".format(err_msg))
return TPE_OATH_MISMATCH, None
del user_info['password']
del user_info['oath_secret']
return TPE_OK, user_info
def get_users(sql_filter, sql_order, sql_limit, sql_restrict, sql_exclude):
dbtp = get_db().table_prefix
s = SQL(get_db())
@ -339,7 +402,19 @@ def update_login_info(handler, user_id):
''.format(db.table_prefix,
login_time=_time_now, ip=handler.request.remote_ip, user_id=user_id
)
db_ret = db.exec(sql)
if db.exec(sql):
return TPE_OK
else:
return TPE_DATABASE
def update_oath_secret(user_id, oath_secret):
db = get_db()
sql = 'UPDATE `{dbtp}user` SET oath_secret="{secret}" WHERE id={user_id}'.format(dbtp=db.table_prefix, secret=oath_secret, user_id=user_id)
if db.exec(sql):
return TPE_OK
else:
return TPE_DATABASE
def update_users_state(handler, user_ids, state):
@ -467,16 +542,6 @@ def remove_users(handler, users):
# return -102
#
#
# def update_oath_secret(user_id, oath_secret):
# db = get_db()
# sql = 'UPDATE `{}account` SET `oath_secret`="{}" WHERE `account_id`={}'.format(db.table_prefix, oath_secret, int(user_id))
# db_ret = db.exec(sql)
# if db_ret:
# return 0
# else:
# return -102
# def get_user_list(with_admin=False):
# db = get_db()
# ret = list()