mirror of https://github.com/jumpserver/jumpserver
perf: 绑定MFA认证密码时对密码进行加密传输 (#6776)
* perf: 绑定MFA认证密码时对密码进行加密传输 * perf: 绑定MFA认证密码时对密码进行加密传输 Co-authored-by: Michael Bai <baijiangjie@gmail.com>pull/6777/head
parent
c1375ed7cb
commit
fca3a8fbca
|
@ -14,14 +14,15 @@ from django.contrib.auth import (
|
||||||
PermissionDenied, user_login_failed, _clean_credentials
|
PermissionDenied, user_login_failed, _clean_credentials
|
||||||
)
|
)
|
||||||
from django.shortcuts import reverse, redirect
|
from django.shortcuts import reverse, redirect
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||||
from . import errors
|
from . import errors
|
||||||
from .utils import rsa_decrypt
|
from .utils import rsa_decrypt, gen_key_pair
|
||||||
from .signals import post_auth_success, post_auth_failed
|
from .signals import post_auth_success, post_auth_failed
|
||||||
from .const import RSA_PRIVATE_KEY
|
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -79,7 +80,70 @@ def authenticate(request=None, **credentials):
|
||||||
auth.authenticate = authenticate
|
auth.authenticate = authenticate
|
||||||
|
|
||||||
|
|
||||||
class AuthMixin:
|
class PasswordEncryptionViewMixin:
|
||||||
|
request = None
|
||||||
|
|
||||||
|
def get_decrypted_password(self, password=None, username=None):
|
||||||
|
request = self.request
|
||||||
|
if hasattr(request, 'data'):
|
||||||
|
data = request.data
|
||||||
|
else:
|
||||||
|
data = request.POST
|
||||||
|
|
||||||
|
username = username or data.get('username')
|
||||||
|
password = password or data.get('password')
|
||||||
|
|
||||||
|
password = self.decrypt_passwd(password)
|
||||||
|
if not password:
|
||||||
|
self.raise_password_decrypt_failed(username=username)
|
||||||
|
return password
|
||||||
|
|
||||||
|
def raise_password_decrypt_failed(self, username):
|
||||||
|
ip = self.get_request_ip()
|
||||||
|
raise errors.CredentialError(
|
||||||
|
error=errors.reason_password_decrypt_failed,
|
||||||
|
username=username, ip=ip, request=self.request
|
||||||
|
)
|
||||||
|
|
||||||
|
def decrypt_passwd(self, raw_passwd):
|
||||||
|
# 获取解密密钥,对密码进行解密
|
||||||
|
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||||
|
if rsa_private_key is not None:
|
||||||
|
try:
|
||||||
|
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e, exc_info=True)
|
||||||
|
logger.error(
|
||||||
|
f'Decrypt password failed: password[{raw_passwd}] '
|
||||||
|
f'rsa_private_key[{rsa_private_key}]'
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return raw_passwd
|
||||||
|
|
||||||
|
def get_request_ip(self):
|
||||||
|
ip = ''
|
||||||
|
if hasattr(self.request, 'data'):
|
||||||
|
ip = self.request.data.get('remote_addr', '')
|
||||||
|
ip = ip or get_request_ip(self.request)
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||||
|
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||||
|
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||||
|
if not all((rsa_private_key, rsa_public_key)):
|
||||||
|
rsa_private_key, rsa_public_key = gen_key_pair()
|
||||||
|
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
||||||
|
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
||||||
|
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
|
||||||
|
|
||||||
|
kwargs.update({
|
||||||
|
'rsa_public_key': rsa_public_key,
|
||||||
|
})
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMixin(PasswordEncryptionViewMixin):
|
||||||
request = None
|
request = None
|
||||||
partial_credential_error = None
|
partial_credential_error = None
|
||||||
|
|
||||||
|
@ -106,13 +170,6 @@ class AuthMixin:
|
||||||
user.backend = self.request.session.get("auth_backend")
|
user.backend = self.request.session.get("auth_backend")
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_request_ip(self):
|
|
||||||
ip = ''
|
|
||||||
if hasattr(self.request, 'data'):
|
|
||||||
ip = self.request.data.get('remote_addr', '')
|
|
||||||
ip = ip or get_request_ip(self.request)
|
|
||||||
return ip
|
|
||||||
|
|
||||||
def _check_is_block(self, username, raise_exception=True):
|
def _check_is_block(self, username, raise_exception=True):
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
if LoginBlockUtil(username, ip).is_block():
|
if LoginBlockUtil(username, ip).is_block():
|
||||||
|
@ -130,19 +187,6 @@ class AuthMixin:
|
||||||
username = self.request.POST.get("username")
|
username = self.request.POST.get("username")
|
||||||
self._check_is_block(username, raise_exception)
|
self._check_is_block(username, raise_exception)
|
||||||
|
|
||||||
def decrypt_passwd(self, raw_passwd):
|
|
||||||
# 获取解密密钥,对密码进行解密
|
|
||||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
|
||||||
if rsa_private_key is not None:
|
|
||||||
try:
|
|
||||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e, exc_info=True)
|
|
||||||
logger.error(f'Decrypt password failed: password[{raw_passwd}] '
|
|
||||||
f'rsa_private_key[{rsa_private_key}]')
|
|
||||||
return None
|
|
||||||
return raw_passwd
|
|
||||||
|
|
||||||
def raise_credential_error(self, error):
|
def raise_credential_error(self, error):
|
||||||
raise self.partial_credential_error(error=error)
|
raise self.partial_credential_error(error=error)
|
||||||
|
|
||||||
|
@ -158,14 +202,12 @@ class AuthMixin:
|
||||||
|
|
||||||
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
|
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
|
||||||
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
|
||||||
password = password + challenge.strip()
|
|
||||||
ip = self.get_request_ip()
|
ip = self.get_request_ip()
|
||||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||||
|
|
||||||
|
password = password + challenge.strip()
|
||||||
if decrypt_passwd:
|
if decrypt_passwd:
|
||||||
password = self.decrypt_passwd(password)
|
password = self.get_decrypted_password()
|
||||||
if not password:
|
|
||||||
self.raise_credential_error(errors.reason_password_decrypt_failed)
|
|
||||||
return username, password, public_key, ip, auto_login
|
return username, password, public_key, ip, auto_login
|
||||||
|
|
||||||
def _check_only_allow_exists_user_auth(self, username):
|
def _check_only_allow_exists_user_auth(self, username):
|
||||||
|
|
|
@ -137,15 +137,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
self.request.session[RSA_PUBLIC_KEY] = None
|
self.request.session[RSA_PUBLIC_KEY] = None
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
|
||||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
|
||||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
|
||||||
if not all((rsa_private_key, rsa_public_key)):
|
|
||||||
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
|
||||||
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
|
||||||
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
|
||||||
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
|
|
||||||
|
|
||||||
forgot_password_url = reverse('authentication:forgot-password')
|
forgot_password_url = reverse('authentication:forgot-password')
|
||||||
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
|
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
|
||||||
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
|
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
|
||||||
|
@ -158,7 +149,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||||
'AUTH_FEISHU': settings.AUTH_FEISHU,
|
'AUTH_FEISHU': settings.AUTH_FEISHU,
|
||||||
'rsa_public_key': rsa_public_key,
|
|
||||||
'forgot_password_url': forgot_password_url
|
'forgot_password_url': forgot_password_url
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
|
|
|
@ -19,7 +19,7 @@ __all__ = [
|
||||||
class UserCheckPasswordForm(forms.Form):
|
class UserCheckPasswordForm(forms.Form):
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label=_('Password'), widget=forms.PasswordInput,
|
label=_('Password'), widget=forms.PasswordInput,
|
||||||
max_length=128, strip=False
|
max_length=1024, strip=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,34 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="" role="form" method="post" action="">
|
<form id="verify-form" class="" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-input">
|
<div class="form-input">
|
||||||
<input type="password" class="" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="" autofocus="autofocus">
|
<input id="password" type="password" class="" placeholder="{% trans 'Password' %}" required="" autofocus="autofocus">
|
||||||
|
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="next">{% trans 'Next' %}</button>
|
<button type="submit" class="next" onclick="doVerify();return false;">{% trans 'Next' %}</button>
|
||||||
{% if 'password' in form.errors %}
|
{% if 'password' in form.errors %}
|
||||||
<p class="red-fonts">{{ form.password.errors.as_text }}</p>
|
<p class="red-fonts">{{ form.password.errors.as_text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function encryptLoginPassword(password, rsaPublicKey) {
|
||||||
|
var jsencrypt = new JSEncrypt(); //加密对象
|
||||||
|
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||||
|
return jsencrypt.encrypt(password); //加密
|
||||||
|
}
|
||||||
|
function doVerify() {
|
||||||
|
//公钥加密
|
||||||
|
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||||
|
var password = $('#password').val(); //明文密码
|
||||||
|
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||||
|
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||||
|
$('#verify-form').submit();//post提交
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,6 +6,8 @@ from django.contrib.auth import authenticate
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
from authentication.mixins import PasswordEncryptionViewMixin
|
||||||
|
from authentication import errors
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from ... import forms
|
from ... import forms
|
||||||
|
@ -18,13 +20,17 @@ __all__ = ['UserVerifyPasswordView']
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserVerifyPasswordView(FormView):
|
class UserVerifyPasswordView(PasswordEncryptionViewMixin, FormView):
|
||||||
template_name = 'users/user_password_verify.html'
|
template_name = 'users/user_password_verify.html'
|
||||||
form_class = forms.UserCheckPasswordForm
|
form_class = forms.UserCheckPasswordForm
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
user = get_user_or_pre_auth_user(self.request)
|
user = get_user_or_pre_auth_user(self.request)
|
||||||
password = form.cleaned_data.get('password')
|
try:
|
||||||
|
password = self.get_decrypted_password(username=user.username)
|
||||||
|
except errors.AuthFailedError as e:
|
||||||
|
form.add_error("password", _(f"Password invalid") + f'({e.msg})')
|
||||||
|
return self.form_invalid(form)
|
||||||
user = authenticate(request=self.request, username=user.username, password=password)
|
user = authenticate(request=self.request, username=user.username, password=password)
|
||||||
if not user:
|
if not user:
|
||||||
form.add_error("password", _("Password invalid"))
|
form.add_error("password", _("Password invalid"))
|
||||||
|
|
Loading…
Reference in New Issue