二次验证,加强安全登录

pull/526/head
Mr Chen 1 year ago
parent a2d0cc6cfe
commit d19e9cd8f4
  1. 3
      .gitignore
  2. 108
      class/core/config_api.py
  3. 46
      route/__init__.py
  4. 58
      route/static/app/config.js
  5. 8
      route/templates/default/config.html
  6. 1
      route/templates/default/layout.html
  7. 51
      route/templates/default/login.html

3
.gitignore vendored

@ -154,6 +154,7 @@ data/notify.json
data/api.json
data/bind_domain.pl
data/unauthorized_status.pl
data/auth_secret.pl
plugins/vip_*
plugins/own_*
@ -175,7 +176,5 @@ plugins/proxysql
debug.out
mdioks.session-journal
*.session

@ -31,6 +31,23 @@ class config_api:
__version = '0.16.3'
__api_addr = 'data/api.json'
# 统一默认配置文件
__file = {
'api' : 'data/api.json', # API文件
'debug' : 'data/debug.pl', # DEBUG文件
'close' : 'data/close.pl', # 识别关闭面板文件
'basic_auth' : 'data/basic_auth.json', # 面板Basic验证
'admin_path' : 'data/admin_path.pl', # 面板后缀路径设置
'ipv6' : 'data/ipv6.pl', # ipv6识别文件
'bind_domain' : 'data/bind_domain.pl', # 面板域名绑定
'unauth_status' : 'data/unauthorized_status.pl', # URL路径未成功显示状态
'auth_secret': 'data/auth_secret.pl', # 二次验证密钥
'ssl':'data/ssl.pl', # ssl设置
'hook_database' : 'data/hook_database.json', # 数据库钩子
'hook_menu' : 'data/hook_menu.json', # 菜单钩子
'hook_global_static' : 'data/hook_global_static.json', # 静态文件钩子
}
def __init__(self):
pass
@ -179,7 +196,7 @@ class config_api:
basic_open = request.form.get('is_open', '').strip()
salt = '_md_salt'
path = 'data/basic_auth.json'
path = self.__file['basic_auth']
is_open = True
if basic_open == 'false':
@ -300,7 +317,7 @@ class config_api:
# '警告,关闭安全入口等于直接暴露你的后台地址在外网,十分危险,至少开启以下一种安全方式才能关闭:<a
# style="color:red;"><br>1、绑定访问域名<br>2、绑定授权IP</a>')
admin_path_file = 'data/admin_path.pl'
admin_path_file = self.__file['admin_path']
admin_path_old = '/'
if os.path.exists(admin_path_file):
admin_path_old = mw.readFile(admin_path_file).strip()
@ -311,7 +328,7 @@ class config_api:
return mw.returnJson(True, '修改成功!')
def closePanelApi(self):
filename = 'data/close.pl'
filename = self.__file['close']
if os.path.exists(filename):
os.remove(filename)
return mw.returnJson(True, '开启成功')
@ -329,8 +346,8 @@ class config_api:
return mw.returnJson(True, '开发模式开启!')
def setIpv6StatusApi(self):
ipv6_file = 'data/ipv6.pl'
if os.path.exists('data/ipv6.pl'):
ipv6_file = self.__file['ipv6']
if os.path.exists(ipv6_file):
os.remove(ipv6_file)
mw.writeLog('面板设置', '关闭面板IPv6兼容!')
else:
@ -400,7 +417,7 @@ class config_api:
# 设置面板SSL证书设置
def setPanelHttpToHttpsApi(self):
bind_domain = 'data/bind_domain.pl'
bind_domain = self.__file['bind_domain']
if not os.path.exists(bind_domain):
return mw.returnJson(False, '先要绑定域名!')
@ -445,7 +462,7 @@ class config_api:
# 删除面板证书
def delPanelSslApi(self):
bind_domain = 'data/bind_domain.pl'
bind_domain = self.__file['bind_domain']
if not os.path.exists(bind_domain):
return mw.returnJson(False, '未绑定域名!')
@ -474,7 +491,7 @@ class config_api:
def applyPanelLetSslApi(self):
# check domain is bind?
bind_domain = 'data/bind_domain.pl'
bind_domain = self.__file['bind_domain']
if not os.path.exists(bind_domain):
return mw.returnJson(False, '先要绑定域名!')
@ -531,7 +548,7 @@ class config_api:
panel_tpl = mw.getRunDir() + "/data/tpl/nginx_panel.conf"
dst_panel_path = mw.getServerDir() + "/web_conf/nginx/vhost/panel.conf"
cfg_domain = 'data/bind_domain.pl'
cfg_domain = self.__file['bind_domain']
if domain == '':
os.remove(cfg_domain)
os.remove(dst_panel_path)
@ -560,7 +577,7 @@ class config_api:
# 设置面板SSL
def setPanelSslApi(self):
sslConf = mw.getRunDir() + '/data/ssl.pl'
sslConf = mw.getRunDir() + '/' + self.__file['ssl']
panel_tpl = mw.getRunDir() + "/data/tpl/nginx_panel.conf"
dst_panel_path = mw.getServerDir() + "/web_conf/nginx/vhost/panel.conf"
@ -751,8 +768,8 @@ class config_api:
return mw.returnJson(False, '状态码范围错误!')
else:
return mw.returnJson(False, '状态码范围错误!')
mw.writeFile('data/unauthorized_status.pl', str(status_code))
unauthorized_status = self.__file['unauth_status']
mw.writeFile(unauthorized_status, str(status_code))
mw.writeLog('面板设置', '将未授权响应状态码设置为:{}'.format(status_code))
return mw.returnJson(True, '设置成功!')
@ -871,8 +888,7 @@ class config_api:
if not 'token_crypt' in data:
token = mw.getRandomString(32)
data['token'] = mw.md5(token)
data['token_crypt'] = mw.enCrypt(
data['token'], token).decode('utf-8')
data['token_crypt'] = mw.enCrypt(data['token'], token).decode('utf-8')
token = stats[data['open']] + '成功!'
mw.writeLog('API配置', '%sAPI接口' % stats[data['open']])
@ -888,7 +904,7 @@ class config_api:
return mw.returnJson(True, '保存成功!')
def renderUnauthorizedStatus(self, data):
cfg_unauth_status = 'data/unauthorized_status.pl'
cfg_unauth_status = self.__file['unauth_status']
if os.path.exists(cfg_unauth_status):
status_code = mw.readFile(cfg_unauth_status)
data['status_code'] = status_code
@ -927,6 +943,37 @@ class config_api:
return mw.returnJson(True, '设置成功!')
def getAuthSecretApi(self):
reset = request.form.get('reset', '')
import pyotp
auth = self.__file['auth_secret']
tag = 'mdserver-web'
if os.path.exists(auth) and reset != '1':
content = mw.readFile(auth)
sec = mw.deDoubleCrypt(tag,content)
else:
sec = pyotp.random_base32()
crypt_data = mw.enDoubleCrypt(tag, sec)
mw.writeFile(auth, crypt_data)
ip = mw.getHostAddr()
url = pyotp.totp.TOTP(sec).provisioning_uri(name=ip, issuer_name='mdserver-web')
rdata = {}
rdata['secret'] = sec
rdata['url'] = url
return mw.returnJson(True, '设置成功!', rdata)
def setAuthSecretApi(self):
auth = self.__file['auth_secret']
if os.path.exists(auth):
os.remove(auth)
return mw.returnJson(True, '关闭成功!', 0)
else:
return mw.returnJson(True, '开启成功!', 1)
def get(self):
data = {}
@ -939,31 +986,38 @@ class config_api:
data['port'] = mw.getHostPort()
data['ip'] = mw.getHostAddr()
admin_path_file = 'data/admin_path.pl'
admin_path_file = self.__file['admin_path']
if not os.path.exists(admin_path_file):
data['admin_path'] = '/'
else:
data['admin_path'] = mw.readFile(admin_path_file)
ipv6_file = 'data/ipv6.pl'
ipv6_file = self.__file['ipv6']
if os.path.exists(ipv6_file):
data['ipv6'] = 'checked'
else:
data['ipv6'] = ''
debug_file = 'data/debug.pl'
debug_file = self.__file['debug']
if os.path.exists(debug_file):
data['debug'] = 'checked'
else:
data['debug'] = ''
ssl_file = 'data/ssl.pl'
if os.path.exists('data/ssl.pl'):
ssl_file = self.__file['ssl']
if os.path.exists(ssl_file):
data['ssl'] = 'checked'
else:
data['ssl'] = ''
basic_auth = 'data/basic_auth.json'
auth_secret = self.__file['auth_secret']
if os.path.exists(auth_secret):
data['auth_secret'] = 'checked'
else:
data['auth_secret'] = ''
basic_auth = self.__file['basic_auth']
if os.path.exists(basic_auth):
bac = mw.readFile(basic_auth)
bac = json.loads(bac)
@ -972,7 +1026,7 @@ class config_api:
else:
data['basic_auth'] = ''
cfg_domain = 'data/bind_domain.pl'
cfg_domain = self.__file['bind_domain']
if os.path.exists(cfg_domain):
domain = mw.readFile(cfg_domain)
data['bind_domain'] = domain.strip()
@ -981,6 +1035,7 @@ class config_api:
data = self.renderUnauthorizedStatus(data)
#api
api_token = self.__api_addr
if os.path.exists(api_token):
bac = mw.readFile(api_token)
@ -990,6 +1045,9 @@ class config_api:
else:
data['api_token'] = ''
#auth
data['site_count'] = mw.M('sites').count()
data['username'] = mw.M('users').where(
@ -998,7 +1056,7 @@ class config_api:
data['hook_tag'] = request.args.get('tag', '')
# databases hook
database_hook_file = 'data/hook_database.json'
database_hook_file = self.__file['hook_database']
if os.path.exists(database_hook_file):
df = mw.readFile(database_hook_file)
df = json.loads(df)
@ -1007,7 +1065,7 @@ class config_api:
data['hook_database'] = []
# menu hook
menu_hook_file = 'data/hook_menu.json'
menu_hook_file = self.__file['hook_menu']
if os.path.exists(menu_hook_file):
df = mw.readFile(menu_hook_file)
df = json.loads(df)
@ -1016,7 +1074,7 @@ class config_api:
data['hook_menu'] = []
# global_static hook
global_static_hook_file = 'data/hook_global_static.json'
global_static_hook_file = self.__file['hook_global_static']
if os.path.exists(global_static_hook_file):
df = mw.readFile(global_static_hook_file)
df = json.loads(df)

@ -187,8 +187,7 @@ def requestAfter(response):
def isLogined():
if 'login' in session and 'username' in session and session['login'] == True:
userInfo = mw.M('users').where(
"id=?", (1,)).field('id,username,password').find()
userInfo = mw.M('users').where("id=?", (1,)).field('id,username,password').find()
# print(userInfo)
if userInfo['username'] != session['username']:
return False
@ -312,6 +311,29 @@ def checkLogin():
return "false"
@app.route("/verify_login", methods=['POST'])
def verifyLogin():
username = request.form.get('username', '').strip()
auth = request.form.get('auth', '').strip()
import pyotp
auth_file = 'data/auth_secret.pl'
if os.path.exists(auth_file):
content = mw.readFile(auth_file)
sec = mw.deDoubleCrypt('mdserver-web', content)
print(sec)
totp = pyotp.TOTP(sec)
if totp.verify(auth):
userInfo = mw.M('users').where("id=?", (1,)).field('id,username,password').find()
session['login'] = True
session['username'] = userInfo['username']
session['overdue'] = int(time.time()) + 7 * 24 * 60 * 60
return mw.returnJson(1, '二次验证成功!')
return mw.returnJson(-1, '二次验证失败!')
@app.route("/do_login", methods=['POST'])
def doLogin():
login_cache_count = 5
@ -343,8 +365,7 @@ def doLogin():
mw.writeLog('用户登录', code_msg)
return mw.returnJson(False, code_msg)
userInfo = mw.M('users').where(
"id=?", (1,)).field('id,username,password').find()
userInfo = mw.M('users').where("id=?", (1,)).field('id,username,password').find()
# print(userInfo)
# print(password)
@ -367,9 +388,16 @@ def doLogin():
cache.set('login_cache_limit', login_cache_limit, timeout=10000)
login_cache_limit = cache.get('login_cache_limit')
mw.writeLog('用户登录', mw.getInfo(msg))
return mw.returnJson(False, mw.getInfo("用户名或密码错误,您还可以尝试[{1}]次!", (str(login_cache_count - login_cache_limit))))
return mw.returnJson(-1, mw.getInfo("用户名或密码错误,您还可以尝试[{1}]次!", (str(login_cache_count - login_cache_limit))))
cache.delete('login_cache_limit')
# 二次验证密钥
auth_secret = 'data/auth_secret.pl'
if os.path.exists(auth_secret):
return mw.returnJson(2, '绑定二次验证了...')
session['login'] = True
session['username'] = userInfo['username']
session['overdue'] = int(time.time()) + 7 * 24 * 60 * 60
@ -377,7 +405,7 @@ def doLogin():
# fix 跳转时,数据消失,可能是跨域问题
# mw.writeFile('data/api_login.txt', userInfo['username'])
return mw.returnJson(True, '登录成功,正在跳转...')
return mw.returnJson(1, '登录成功,正在跳转...')
@app.errorhandler(404)
@ -419,8 +447,7 @@ def login_temp_user(token):
return '连续10次验证失败,禁止1小时'
stime = int(time.time())
data = mw.M('temp_login').where('state=? and expire>?',
(0, stime)).field('id,token,salt,expire,addtime').find()
data = mw.M('temp_login').where('state=? and expire>?',(0, stime)).field('id,token,salt,expire,addtime').find()
if not data:
setErrorNum(skey)
return '验证失败!'
@ -434,8 +461,7 @@ def login_temp_user(token):
setErrorNum(skey)
return '验证失败!'
userInfo = mw.M('users').where(
"id=?", (1,)).field('id,username').find()
userInfo = mw.M('users').where("id=?", (1,)).field('id,username').find()
session['login'] = True
session['username'] = userInfo['username']
session['tmp_login'] = True

@ -1062,6 +1062,64 @@ function setTempAccess(){
});
}
//二次验证
function setAuthBind(){
$.post('/config/get_auth_secret', {}, function(rdata){
console.log(rdata);
var tip = layer.open({
area: ['500px', '355px'],
title: '二次验证设置',
closeBtn:1,
shift: 0,
type: 1,
content: '<div class="bt-form pd20">\
<div class="line">\
<span class="tname">绑定密钥</span>\
<div class="info-r">\
<input class="bt-input-text mr5" name="secret" type="text" style="width: 310px;" disabled>\
<button class="btn btn-success btn-xs reset_secret" style="margin-left: -50px;">重置</button>\
</div>\
</div>\
<div class="line">\
<span class="tname" style="width: 90px; overflow: initial; height: 20px; line-height: 20px;">二维码</span>\
<div class="info-r"><div class="qrcode"></div></div>\
</div>\
<ul class="help-info-text c7">\
</ul>\
</div>',
success:function(layero,index){
$('input[name="secret"]').val(rdata.data['secret']);
$('.qrcode').qrcode({ text: rdata.data['url']});
$('.reset_secret').click(function(){
layer.confirm('您确定要重置当前密钥吗?<br/><span style="color: red; ">重置密钥后,已关联密钥产品,将失效,请重新添加新密钥至产品。</span>',{title:'重置密钥',closeBtn:2,icon:13,cancel:function(){
}}, function() {
$.post('/config/get_auth_secret', {'reset':"1"},function(rdata){
showMsg("接口密钥已生成,重置密钥后,已关联密钥产品,将失效,请重新添加新密钥至产品。", function(){
$('input[name="secret"]').val(rdata.data['secret']);
$('.qrcode').html('').qrcode({ text: rdata.data['url']});
} ,{icon:1}, 2000);
},'json');
});
});
},
});
},'json');
}
function setAuthSecretApi(){
var cfg_panel_auth = $('#cfg_panel_auth').prop("checked");
$.post('/config/set_auth_secret', {'op_type':"2"},function(rdata){
showMsg(rdata.msg, function(){
if (rdata.data == 1){
setAuthBind();
}
} ,{icon:rdata.status?1:2}, 1000);
},'json');
}
function setBasicAuthTip(callback){
var tip = layer.open({
area: ['500px', '385px'],

@ -146,6 +146,14 @@
<button type="button" class="btn btn-success btn-sm ml5" onclick="setTempAccess()">临时访问授权管理</button>
<span class="set-info c7">为非管理员临时提供面板访问权限</span>
</p>
<p class="mtb15">
<span class="set-tit text-right" title="二次验证" style="float: left;">二次验证</span>
<input class="btswitch btswitch-ios" id="cfg_panel_auth" type="checkbox" {{data['auth_secret']}}/>
<label class="btswitch-btn ml5" for="cfg_panel_auth" style="float: left;margin-top:4px;" onclick="setAuthSecretApi()"></label>
<button type="button" class="btn btn-default btn-xs panel_api_btn" style="vertical-align: middle; margin-left: 10px" onclick="setAuthBind();">绑定设置</button>
<span class="set-info c7">二次验证,加强安全登录</span>
</p>
</div>

@ -92,6 +92,7 @@
<script src="/static/js/jquery.dragsort-0.5.2.min.js"></script>
<script src="/static/js/jquery-qrcode-0.18.0.min.js?v={{config.version}}"></script>
<script src="/static/js/xm-select.js"></script>
{% block content %}{% endblock %}

@ -8,6 +8,7 @@
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon" />
<title>{{data['title']}}</title>
<link rel="stylesheet" type="text/css" href="/static/layer/skin/default/layer.css?v={{config.version}}">
<link rel="stylesheet" type="text/css" href="/static/css/site.css?v={{config.version}}">
<link rel="stylesheet" type="text/css" href="/static/css/login.css?v={{config.version}}">
<style type="text/css">
@ -29,6 +30,7 @@
.bg_img.pc:hover{
background-position: -60px -60px;
}
.qrCode{
text-align: center;
padding-top: 20px;
@ -70,9 +72,11 @@
padding: 20px;
background-color: #fff;
}
.list_scan .weChatSamll img{
width: 100%;
}
.list_scan .weChatSamll em{
position: absolute;
border: 7px solid #ececec;
@ -83,6 +87,7 @@
bottom: -14px;
margin-left: -6px;
}
.tips{
width: 115px;
position: absolute;
@ -94,6 +99,7 @@
text-align: center;
border-radius: 4px;
}
.tips em{
position: absolute;
border: 6px solid #dff0d8;
@ -104,6 +110,7 @@
top: 8px;
margin-left: -6px;
}
.tips img{
height: 16px;
width: 16px;
@ -115,6 +122,10 @@
margin-top: 15px;
margin-bottom: 25px;
}
.layui-layer .layui-layer-btn{
width: 235px;
}
</style>
</head>
<body>
@ -123,11 +134,14 @@
<div class="account">
<form class="loginform" method="post" action="/login" onsubmit="return false;">
<div class="rlogo">{{data['title']}}</div>
<div class="line"><input class="inputtxt" value="" name="username" datatype="*" nullmsg="请填写账户" errormsg="请填写账户" placeholder="账户" type="text"><div class="Validform_checktip"></div></div>
<div class="line">
<input class="inputtxt" value="" name="username" datatype="*" nullmsg="请填写账户" errormsg="请填写账户" placeholder="账户" type="text">
<div class="Validform_checktip"></div>
</div>
<div class="line"><input class="inputtxt" name="password" value="" datatype="*" nullmsg="请填写密码" errormsg="请填写密码" placeholder="密码" type="password">
<div class="Validform_checktip"></div>
</div>
<div style="color: red;position: relative;top: -14px;" id="errorStr"></div>
<div style="color: red;position: relative;top: -14px;" id="error"></div>
<div class="line yzm" style="top: -5px;{% if not session['code'] %}display:none;{% endif %}">
<input type="text" class="inputtxt" name="code" nullmsg="请填写4位验证码" errormsg="验证码" datatype="*" placeholder="验证码">
<div class="Validform_checktip"></div>
@ -150,6 +164,7 @@
$(function(){
wreset();
})
window.onresize=function(){
wreset();
}
@ -188,19 +203,21 @@ $('#login-button').click(function(){
}
var data = 'username='+username+'&password='+password+'&code='+code;
var loadT = layer.msg("正在登录中",{icon:16,time:0,shade: [0.3, '#000']});
var loadT = layer.msg("正在登录中...",{icon:16,time:0,shade: [0.3, '#000']});
$.post('/do_login',data,function(rdata){
console.log(rdata);
layer.close(loadT);
if(!rdata.status){
if(status < 0){
if(username == 'admin' && rdata.msg.indexOf('用户名') != -1){
rdata.msg += ', <br>获取默认用户和密码命令: mw default';
}
$("#errorStr").html(rdata.msg);
$("#error").html(rdata.msg);
$("input[name='password']").val('');
num = rdata.msg.substring(rdata.msg.indexOf('[')+1,rdata.msg.indexOf(']'))
if(num < 5){
$('#mw_yzm').html('<img width="100" height="40" class="passcode" onClick="this.src=this.src.split(\'?\')[0] + \'?\'+new Date().getTime()" src="/code" style="border: 1px solid #ccc; float: right;" title="" >');
$(".login").css("height","332px");
var code_html = '<img width="100" height="40" class="passcode" onClick="this.src=this.src.split(\'?\')[0] + \'?\'+new Date().getTime()" src="/code" style="border: 1px solid #ccc; float: right;">';
$('#mw_yzm').html(code_html);
// $(".login").css("height","332px");
$("input[name='code']").val('');
$(".passcode").click();
}
@ -210,6 +227,26 @@ $('#login-button').click(function(){
return;
}
if (rdata.status == 2){
layer.prompt({
formType: 3,
value: '',
title: '二次验证',
}, function(value, index, elem){
$.post('/verify_login',{'auth':value,'username':username},function(vdata){
if (vdata.status < 0){
layer.msg(vdata.msg,{icon:2,time:5000});
return;
}
layer.close(index);
window.location.href = '/';
},'json');
});
return;
}
layer.msg(rdata.msg,{icon:16,time:0,shade: [0.3, '#000']});
window.location.href = '/';
},'json');

Loading…
Cancel
Save