diff --git a/apps/common/forms.py b/apps/common/forms.py
index 87b2f4f12..a11420498 100644
--- a/apps/common/forms.py
+++ b/apps/common/forms.py
@@ -170,7 +170,7 @@ class TerminalSettingForm(BaseForm):
class SecuritySettingForm(BaseForm):
- # MFA全局设置
+ # MFA global setting
SECURITY_MFA_AUTH = forms.BooleanField(
initial=False, required=False,
label=_("MFA Secondary certification"),
@@ -179,12 +179,26 @@ class SecuritySettingForm(BaseForm):
'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(
initial=6, label=_("Password minimum length"),
min_value=6
)
- # 大写字母
+ # upper case
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
initial=False, required=False,
@@ -193,21 +207,21 @@ class SecuritySettingForm(BaseForm):
'After opening, the user password changes '
'and resets must contain uppercase letters')
)
- # 小写字母
+ # lower case
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
initial=False, required=False,
label=_("Must contain lowercase letters"),
help_text=_('After opening, the user password changes '
'and resets must contain lowercase letters')
)
- # 数字
+ # number
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
initial=False, required=False,
label=_("Must contain numeric characters"),
help_text=_('After opening, the user password changes '
'and resets must contain numeric characters')
)
- # 特殊字符
+ # special char
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
initial=False, required=False,
label=_("Must contain special characters"),
diff --git a/apps/common/templates/common/security_setting.html b/apps/common/templates/common/security_setting.html
index 2260b76b9..08d978d23 100644
--- a/apps/common/templates/common/security_setting.html
+++ b/apps/common/templates/common/security_setting.html
@@ -39,9 +39,9 @@
{% endif %}
{% csrf_token %}
-
{% trans "MFA setting" %}
+ {% trans "User login settings" %}
{% for field in form %}
- {% if forloop.counter == 2 %}
+ {% if forloop.counter == 4 %}
{% trans "Password check rule" %}
{% endif %}
diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo
index b5474cbe1..c9b42d9f4 100644
Binary files a/apps/i18n/zh/LC_MESSAGES/django.mo and b/apps/i18n/zh/LC_MESSAGES/django.mo differ
diff --git a/apps/i18n/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po
index 56fabe11a..b1e588650 100644
--- a/apps/i18n/zh/LC_MESSAGES/django.po
+++ b/apps/i18n/zh/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\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"
"Last-Translator: ibuler \n"
"Language-Team: Jumpserver team\n"
@@ -1433,45 +1433,60 @@ msgid ""
"for all users, including administrators)"
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"
msgstr "密码最小长度 "
-#: common/forms.py:191
+#: common/forms.py:208
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
-#: common/forms.py:193
+#: common/forms.py:210
msgid ""
"After opening, the user password changes and resets must contain uppercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含大写字母"
-#: common/forms.py:199
+#: common/forms.py:216
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
-#: common/forms.py:200
+#: common/forms.py:217
msgid ""
"After opening, the user password changes and resets must contain lowercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母"
-#: common/forms.py:206
+#: common/forms.py:223
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
-#: common/forms.py:207
+#: common/forms.py:224
msgid ""
"After opening, the user password changes and resets must contain numeric "
"characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符"
-#: common/forms.py:213
+#: common/forms.py:230
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
-#: common/forms.py:214
+#: common/forms.py:231
msgid ""
"After opening, the user password changes and resets must contain special "
"characters"
@@ -1532,8 +1547,8 @@ msgid "Security setting"
msgstr "安全设置"
#: common/templates/common/security_setting.html:42
-msgid "MFA setting"
-msgstr "MFA 设置"
+msgid "User login settings"
+msgstr "用户登录设置"
#: common/templates/common/security_setting.html:46
msgid "Password check rule"
@@ -2307,6 +2322,10 @@ msgid ""
"You should use your ssh client tools connect terminal: {}
{}"
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
msgid "Invalid signature header. No credentials provided."
msgstr ""
@@ -2649,10 +2668,6 @@ msgstr "忘记密码"
msgid "Input your email, that will send a mail to your"
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
-#: users/templates/users/login.html:50
-msgid "Log in frequently and try again later"
-msgstr "登录频繁, 稍后重试"
-
#: users/templates/users/login.html:53
msgid "Captcha invalid"
msgstr "验证码错误"
@@ -3049,7 +3064,7 @@ msgstr "更新用户组"
msgid "User group granted asset"
msgstr "用户组授权资产"
-#: users/views/login.py:74
+#: users/views/login.py:75
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
@@ -3149,3 +3164,6 @@ msgstr "MFA 解绑成功"
#: users/views/user.py:555
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
+
+#~ msgid "MFA setting"
+#~ msgstr "MFA 设置"
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index c8ed1505e..c0a536276 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -405,6 +405,8 @@ TERMINAL_REPLAY_STORAGE = {
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
BOOTSTRAP3 = {
diff --git a/apps/users/api.py b/apps/users/api.py
index dd1b76ba3..cf13b9aea 100644
--- a/apps/users/api.py
+++ b/apps/users/api.py
@@ -3,6 +3,7 @@ import uuid
from django.core.cache import cache
from django.urls import reverse
+from django.utils.translation import ugettext as _
from rest_framework import generics
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 .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \
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.utils import get_logger
@@ -149,7 +151,6 @@ class UserOtpAuthApi(APIView):
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
if not check_otp_code(user.otp_secret_key, otp_code):
- # Write login failed log
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
@@ -159,7 +160,6 @@ class UserOtpAuthApi(APIView):
self.write_login_log(request, data)
return Response({'msg': 'MFA认证失败'}, status=401)
- # Write login success log
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
@@ -196,12 +196,21 @@ class UserOtpAuthApi(APIView):
class UserAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
+ key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
def post(self, 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:
- # Write login failed log
data = {
'username': request.data.get('username', ''),
'mfa': LoginLog.MFA_UNKNOWN,
@@ -209,10 +218,11 @@ class UserAuthApi(APIView):
'status': False
}
self.write_login_log(request, data)
+
+ set_user_login_failed_count_to_cache(key_limit)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
- # Write login success log
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
diff --git a/apps/users/templates/users/login.html b/apps/users/templates/users/login.html
index 7ea824502..6b55a58bf 100644
--- a/apps/users/templates/users/login.html
+++ b/apps/users/templates/users/login.html
@@ -46,7 +46,7 @@