mirror of https://github.com/jumpserver/jumpserver
[Update] 优化登录失败次数限制的逻辑,并添加系统安全设置选项
parent
ff9b1a887f
commit
512fc8f8f0
|
@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
|
||||||
|
|
||||||
|
|
||||||
class SecuritySettingForm(BaseForm):
|
class SecuritySettingForm(BaseForm):
|
||||||
# MFA全局设置
|
# MFA global setting
|
||||||
SECURITY_MFA_AUTH = forms.BooleanField(
|
SECURITY_MFA_AUTH = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("MFA Secondary certification"),
|
label=_("MFA Secondary certification"),
|
||||||
|
@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
|
||||||
'authentication (valid for all users, including administrators)'
|
'authentication (valid for all users, including administrators)'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# 最小长度
|
# limit login count
|
||||||
|
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
|
||||||
|
initial=3, min_value=3,
|
||||||
|
label=_("Limit the number of login failures")
|
||||||
|
)
|
||||||
|
# limit login time
|
||||||
|
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
|
||||||
|
initial=30, min_value=5,
|
||||||
|
label=_("No logon interval"),
|
||||||
|
help_text=_(
|
||||||
|
"Tip :(unit/minute) if the user has failed to log in for a limited "
|
||||||
|
"number of times, no login is allowed during this time interval."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# min length
|
||||||
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||||
initial=6, label=_("Password minimum length"),
|
initial=6, label=_("Password minimum length"),
|
||||||
min_value=6
|
min_value=6
|
||||||
)
|
)
|
||||||
# 大写字母
|
# upper case
|
||||||
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
||||||
|
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
|
@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
|
||||||
'After opening, the user password changes '
|
'After opening, the user password changes '
|
||||||
'and resets must contain uppercase letters')
|
'and resets must contain uppercase letters')
|
||||||
)
|
)
|
||||||
# 小写字母
|
# lower case
|
||||||
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain lowercase letters"),
|
label=_("Must contain lowercase letters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain lowercase letters')
|
'and resets must contain lowercase letters')
|
||||||
)
|
)
|
||||||
# 数字
|
# number
|
||||||
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain numeric characters"),
|
label=_("Must contain numeric characters"),
|
||||||
help_text=_('After opening, the user password changes '
|
help_text=_('After opening, the user password changes '
|
||||||
'and resets must contain numeric characters')
|
'and resets must contain numeric characters')
|
||||||
)
|
)
|
||||||
# 特殊字符
|
# special char
|
||||||
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
||||||
initial=False, required=False,
|
initial=False, required=False,
|
||||||
label=_("Must contain special characters"),
|
label=_("Must contain special characters"),
|
||||||
|
|
|
@ -39,9 +39,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<h3>{% trans "MFA setting" %}</h3>
|
<h3>{% trans "User login settings" %}</h3>
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% if forloop.counter == 2 %}
|
{% if forloop.counter == 4 %}
|
||||||
<div class="hr-line-dashed"></div>
|
<div class="hr-line-dashed"></div>
|
||||||
<h3>{% trans "Password check rule" %}</h3>
|
<h3>{% trans "Password check rule" %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Jumpserver 0.3.3\n"
|
"Project-Id-Version: Jumpserver 0.3.3\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-07-04 16:46+0800\n"
|
"POT-Creation-Date: 2018-07-05 14:58+0800\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
"Last-Translator: ibuler <ibuler@qq.com>\n"
|
||||||
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
|
||||||
|
@ -1433,45 +1433,60 @@ msgid ""
|
||||||
"for all users, including administrators)"
|
"for all users, including administrators)"
|
||||||
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
|
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
|
||||||
|
|
||||||
#: common/forms.py:184
|
#: common/forms.py:186
|
||||||
|
msgid "Limit the number of login failures"
|
||||||
|
msgstr "限制登录失败次数"
|
||||||
|
|
||||||
|
#: common/forms.py:193
|
||||||
|
msgid "No logon interval"
|
||||||
|
msgstr "禁止登录时间间隔"
|
||||||
|
|
||||||
|
#: common/forms.py:195
|
||||||
|
msgid ""
|
||||||
|
"Tip :(unit/minute) if the user has failed to log in for a limited number of "
|
||||||
|
"times, no login is allowed during this time interval."
|
||||||
|
msgstr ""
|
||||||
|
"提示:(单位 / 分钟)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录."
|
||||||
|
|
||||||
|
#: common/forms.py:201
|
||||||
msgid "Password minimum length"
|
msgid "Password minimum length"
|
||||||
msgstr "密码最小长度 "
|
msgstr "密码最小长度 "
|
||||||
|
|
||||||
#: common/forms.py:191
|
#: common/forms.py:208
|
||||||
msgid "Must contain capital letters"
|
msgid "Must contain capital letters"
|
||||||
msgstr "必须包含大写字母"
|
msgstr "必须包含大写字母"
|
||||||
|
|
||||||
#: common/forms.py:193
|
#: common/forms.py:210
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain uppercase "
|
"After opening, the user password changes and resets must contain uppercase "
|
||||||
"letters"
|
"letters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含大写字母"
|
msgstr "开启后,用户密码修改、重置必须包含大写字母"
|
||||||
|
|
||||||
#: common/forms.py:199
|
#: common/forms.py:216
|
||||||
msgid "Must contain lowercase letters"
|
msgid "Must contain lowercase letters"
|
||||||
msgstr "必须包含小写字母"
|
msgstr "必须包含小写字母"
|
||||||
|
|
||||||
#: common/forms.py:200
|
#: common/forms.py:217
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain lowercase "
|
"After opening, the user password changes and resets must contain lowercase "
|
||||||
"letters"
|
"letters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含小写字母"
|
msgstr "开启后,用户密码修改、重置必须包含小写字母"
|
||||||
|
|
||||||
#: common/forms.py:206
|
#: common/forms.py:223
|
||||||
msgid "Must contain numeric characters"
|
msgid "Must contain numeric characters"
|
||||||
msgstr "必须包含数字字符"
|
msgstr "必须包含数字字符"
|
||||||
|
|
||||||
#: common/forms.py:207
|
#: common/forms.py:224
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain numeric "
|
"After opening, the user password changes and resets must contain numeric "
|
||||||
"characters"
|
"characters"
|
||||||
msgstr "开启后,用户密码修改、重置必须包含数字字符"
|
msgstr "开启后,用户密码修改、重置必须包含数字字符"
|
||||||
|
|
||||||
#: common/forms.py:213
|
#: common/forms.py:230
|
||||||
msgid "Must contain special characters"
|
msgid "Must contain special characters"
|
||||||
msgstr "必须包含特殊字符"
|
msgstr "必须包含特殊字符"
|
||||||
|
|
||||||
#: common/forms.py:214
|
#: common/forms.py:231
|
||||||
msgid ""
|
msgid ""
|
||||||
"After opening, the user password changes and resets must contain special "
|
"After opening, the user password changes and resets must contain special "
|
||||||
"characters"
|
"characters"
|
||||||
|
@ -1532,8 +1547,8 @@ msgid "Security setting"
|
||||||
msgstr "安全设置"
|
msgstr "安全设置"
|
||||||
|
|
||||||
#: common/templates/common/security_setting.html:42
|
#: common/templates/common/security_setting.html:42
|
||||||
msgid "MFA setting"
|
msgid "User login settings"
|
||||||
msgstr "MFA 设置"
|
msgstr "用户登录设置"
|
||||||
|
|
||||||
#: common/templates/common/security_setting.html:46
|
#: common/templates/common/security_setting.html:46
|
||||||
msgid "Password check rule"
|
msgid "Password check rule"
|
||||||
|
@ -2307,6 +2322,10 @@ msgid ""
|
||||||
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
|
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
|
||||||
msgstr "你可以使用ssh客户端工具连接终端"
|
msgstr "你可以使用ssh客户端工具连接终端"
|
||||||
|
|
||||||
|
#: users/api.py:210 users/templates/users/login.html:50
|
||||||
|
msgid "Log in frequently and try again later"
|
||||||
|
msgstr "登录频繁, 稍后重试"
|
||||||
|
|
||||||
#: users/authentication.py:56
|
#: users/authentication.py:56
|
||||||
msgid "Invalid signature header. No credentials provided."
|
msgid "Invalid signature header. No credentials provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2649,10 +2668,6 @@ msgstr "忘记密码"
|
||||||
msgid "Input your email, that will send a mail to your"
|
msgid "Input your email, that will send a mail to your"
|
||||||
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
|
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
|
||||||
|
|
||||||
#: users/templates/users/login.html:50
|
|
||||||
msgid "Log in frequently and try again later"
|
|
||||||
msgstr "登录频繁, 稍后重试"
|
|
||||||
|
|
||||||
#: users/templates/users/login.html:53
|
#: users/templates/users/login.html:53
|
||||||
msgid "Captcha invalid"
|
msgid "Captcha invalid"
|
||||||
msgstr "验证码错误"
|
msgstr "验证码错误"
|
||||||
|
@ -3049,7 +3064,7 @@ msgstr "更新用户组"
|
||||||
msgid "User group granted asset"
|
msgid "User group granted asset"
|
||||||
msgstr "用户组授权资产"
|
msgstr "用户组授权资产"
|
||||||
|
|
||||||
#: users/views/login.py:74
|
#: users/views/login.py:75
|
||||||
msgid "Please enable cookies and try again."
|
msgid "Please enable cookies and try again."
|
||||||
msgstr "设置你的浏览器支持cookie"
|
msgstr "设置你的浏览器支持cookie"
|
||||||
|
|
||||||
|
@ -3149,3 +3164,6 @@ msgstr "MFA 解绑成功"
|
||||||
#: users/views/user.py:555
|
#: users/views/user.py:555
|
||||||
msgid "MFA disable success, return login page"
|
msgid "MFA disable success, return login page"
|
||||||
msgstr "MFA 解绑成功,返回登录页面"
|
msgstr "MFA 解绑成功,返回登录页面"
|
||||||
|
|
||||||
|
#~ msgid "MFA setting"
|
||||||
|
#~ msgstr "MFA 设置"
|
||||||
|
|
|
@ -405,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
||||||
|
DEFAULT_LOGIN_LIMIT_COUNT = 3
|
||||||
|
DEFAULT_LOGIN_LIMIT_TIME = 30
|
||||||
|
|
||||||
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
||||||
BOOTSTRAP3 = {
|
BOOTSTRAP3 = {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import uuid
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
@ -17,7 +18,8 @@ from .tasks import write_login_log_async
|
||||||
from .models import User, UserGroup, LoginLog
|
from .models import User, UserGroup, LoginLog
|
||||||
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
|
||||||
IsSuperUserOrAppUser
|
IsSuperUserOrAppUser
|
||||||
from .utils import check_user_valid, generate_token, get_login_ip, check_otp_code
|
from .utils import check_user_valid, generate_token, get_login_ip, \
|
||||||
|
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
|
||||||
from common.mixins import IDInFilterMixin
|
from common.mixins import IDInFilterMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
@ -149,7 +151,6 @@ class UserOtpAuthApi(APIView):
|
||||||
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
||||||
|
|
||||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||||
# Write login failed log
|
|
||||||
data = {
|
data = {
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'mfa': int(user.otp_enabled),
|
'mfa': int(user.otp_enabled),
|
||||||
|
@ -159,7 +160,6 @@ class UserOtpAuthApi(APIView):
|
||||||
self.write_login_log(request, data)
|
self.write_login_log(request, data)
|
||||||
return Response({'msg': 'MFA认证失败'}, status=401)
|
return Response({'msg': 'MFA认证失败'}, status=401)
|
||||||
|
|
||||||
# Write login success log
|
|
||||||
data = {
|
data = {
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'mfa': int(user.otp_enabled),
|
'mfa': int(user.otp_enabled),
|
||||||
|
@ -196,12 +196,21 @@ class UserOtpAuthApi(APIView):
|
||||||
class UserAuthApi(APIView):
|
class UserAuthApi(APIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
user, msg = self.check_user_valid(request)
|
user, msg = self.check_user_valid(request)
|
||||||
|
|
||||||
|
username = request.data.get('username')
|
||||||
|
ip = request.data.get('remote_addr', None)
|
||||||
|
if not ip:
|
||||||
|
ip = get_login_ip(request)
|
||||||
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
|
if is_block_login(key_limit):
|
||||||
|
msg = _("Log in frequently and try again later")
|
||||||
|
return Response({'msg': msg}, status=401)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
# Write login failed log
|
|
||||||
data = {
|
data = {
|
||||||
'username': request.data.get('username', ''),
|
'username': request.data.get('username', ''),
|
||||||
'mfa': LoginLog.MFA_UNKNOWN,
|
'mfa': LoginLog.MFA_UNKNOWN,
|
||||||
|
@ -209,10 +218,11 @@ class UserAuthApi(APIView):
|
||||||
'status': False
|
'status': False
|
||||||
}
|
}
|
||||||
self.write_login_log(request, data)
|
self.write_login_log(request, data)
|
||||||
|
|
||||||
|
set_user_login_failed_count_to_cache(key_limit)
|
||||||
return Response({'msg': msg}, status=401)
|
return Response({'msg': msg}, status=401)
|
||||||
|
|
||||||
if not user.otp_enabled:
|
if not user.otp_enabled:
|
||||||
# Write login success log
|
|
||||||
data = {
|
data = {
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'mfa': int(user.otp_enabled),
|
'mfa': int(user.otp_enabled),
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<form class="m-t" role="form" method="post" action="">
|
<form class="m-t" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% if login_limit %}
|
{% if block_login %}
|
||||||
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
|
||||||
{% elif form.errors %}
|
{% elif form.errors %}
|
||||||
{% if 'captcha' in form.errors %}
|
{% if 'captcha' in form.errors %}
|
||||||
|
|
|
@ -332,3 +332,29 @@ def check_password_rules(password):
|
||||||
|
|
||||||
match_obj = re.match(pattern, password)
|
match_obj = re.match(pattern, password)
|
||||||
return bool(match_obj)
|
return bool(match_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_login_failed_count_to_cache(key_limit):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
count = count + 1 if count else 1
|
||||||
|
|
||||||
|
setting_limit_time = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_TIME'
|
||||||
|
).first()
|
||||||
|
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||||
|
|
||||||
|
cache.set(key_limit, count, int(limit_time)*60)
|
||||||
|
|
||||||
|
|
||||||
|
def is_block_login(key_limit):
|
||||||
|
count = cache.get(key_limit)
|
||||||
|
|
||||||
|
setting_limit_count = Setting.objects.filter(
|
||||||
|
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||||
|
).first()
|
||||||
|
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||||
|
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||||
|
|
||||||
|
if count and count >= limit_count:
|
||||||
|
return True
|
||||||
|
|
|
@ -27,7 +27,8 @@ from common.models import Setting
|
||||||
from ..models import User, LoginLog
|
from ..models import User, LoginLog
|
||||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
||||||
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||||
set_tmp_user_to_cache, get_password_check_rules, check_password_rules
|
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||||
|
is_block_login, set_user_login_failed_count_to_cache
|
||||||
from ..tasks import write_login_log_async
|
from ..tasks import write_login_log_async
|
||||||
from .. import forms
|
from .. import forms
|
||||||
|
|
||||||
|
@ -63,9 +64,9 @@ class UserLoginView(FormView):
|
||||||
# limit login authentication
|
# limit login authentication
|
||||||
ip = get_login_ip(request)
|
ip = get_login_ip(request)
|
||||||
username = self.request.POST.get('username')
|
username = self.request.POST.get('username')
|
||||||
count = cache.get(self.key_prefix_limit.format(ip, username))
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
if count and count >= 3:
|
if is_block_login(key_limit):
|
||||||
return self.render_to_response(self.get_context_data(login_limit=True))
|
return self.render_to_response(self.get_context_data(block_login=True))
|
||||||
|
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -87,13 +88,12 @@ class UserLoginView(FormView):
|
||||||
}
|
}
|
||||||
self.write_login_log(data)
|
self.write_login_log(data)
|
||||||
|
|
||||||
# limit user login failed times
|
# limit user login failed count
|
||||||
ip = get_login_ip(self.request)
|
ip = get_login_ip(self.request)
|
||||||
key_limit = self.key_prefix_limit.format(ip, username)
|
key_limit = self.key_prefix_limit.format(ip, username)
|
||||||
count = cache.get(key_limit)
|
set_user_login_failed_count_to_cache(key_limit)
|
||||||
count = count + 1 if count else 1
|
|
||||||
cache.set(key_limit, count, 1800)
|
|
||||||
|
|
||||||
|
# show captcha
|
||||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||||
old_form = form
|
old_form = form
|
||||||
form = self.form_class_captcha(data=form.data)
|
form = self.form_class_captcha(data=form.data)
|
||||||
|
|
Loading…
Reference in New Issue