Skip to content

Commit

Permalink
二次验证,加强安全登录
Browse files Browse the repository at this point in the history
  • Loading branch information
midoks committed Mar 10, 2024
1 parent a2d0cc6 commit d19e9cd
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 46 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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_*
Expand All @@ -175,7 +176,5 @@ plugins/proxysql
debug.out




mdioks.session-journal
*.session
108 changes: 83 additions & 25 deletions class/core/config_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -179,7 +196,7 @@ def setBasicAuthApi(self):
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':
Expand Down Expand Up @@ -300,7 +317,7 @@ def setAdminPathApi(self):
# '警告,关闭安全入口等于直接暴露你的后台地址在外网,十分危险,至少开启以下一种安全方式才能关闭:<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()
Expand All @@ -311,7 +328,7 @@ def setAdminPathApi(self):
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, '开启成功')
Expand All @@ -329,8 +346,8 @@ def openDebugApi(self):
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:
Expand Down Expand Up @@ -400,7 +417,7 @@ def savePanelSslApi(self):
# 设置面板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, '先要绑定域名!')

Expand Down Expand Up @@ -445,7 +462,7 @@ def setPanelHttpToHttpsApi(self):

# 删除面板证书
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, '未绑定域名!')

Expand Down Expand Up @@ -474,7 +491,7 @@ def delPanelSslApi(self):
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, '先要绑定域名!')

Expand Down Expand Up @@ -531,7 +548,7 @@ def setPanelDomainApi(self):
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)
Expand Down Expand Up @@ -560,7 +577,7 @@ def setPanelDomainApi(self):

# 设置面板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"
Expand Down Expand Up @@ -751,8 +768,8 @@ def setStatusCodeApi(self):
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, '设置成功!')

Expand Down Expand Up @@ -871,8 +888,7 @@ def setPanelTokenApi(self):
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']])
Expand All @@ -888,7 +904,7 @@ def setPanelTokenApi(self):
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
Expand Down Expand Up @@ -927,6 +943,37 @@ def setTimezoneApi(self):
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 = {}
Expand All @@ -939,31 +986,38 @@ def get(self):
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)
Expand All @@ -972,7 +1026,7 @@ def get(self):
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()
Expand All @@ -981,6 +1035,7 @@ def get(self):

data = self.renderUnauthorizedStatus(data)

#api
api_token = self.__api_addr
if os.path.exists(api_token):
bac = mw.readFile(api_token)
Expand All @@ -990,6 +1045,9 @@ def get(self):
else:
data['api_token'] = ''

#auth


data['site_count'] = mw.M('sites').count()

data['username'] = mw.M('users').where(
Expand All @@ -998,7 +1056,7 @@ def get(self):
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)
Expand All @@ -1007,7 +1065,7 @@ def get(self):
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)
Expand All @@ -1016,7 +1074,7 @@ def get(self):
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)
Expand Down
46 changes: 36 additions & 10 deletions route/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -367,17 +388,24 @@ 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
# session['overdue'] = int(time.time()) + 7

# fix 跳转时,数据消失,可能是跨域问题
# mw.writeFile('data/api_login.txt', userInfo['username'])
return mw.returnJson(True, '登录成功,正在跳转...')
return mw.returnJson(1, '登录成功,正在跳转...')


@app.errorhandler(404)
Expand Down Expand Up @@ -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 '验证失败!'
Expand All @@ -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
Expand Down
Loading

0 comments on commit d19e9cd

Please sign in to comment.