mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-22 11:58:29 +00:00
perf: password 等使用 rsa 加密传输 (#8188)
* perf: 修改 model fields 路径 * stash it * pref: 统一加密方式,密码字段采用 rsa 加密 * pref: 临时密码使用 rsa * perf: 去掉 debug msg * perf: 去掉 Debug * perf: 去掉 debug * perf: 抽出来 Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
@@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
||||
def create(self, request, *args, **kwargs):
|
||||
self.create_session_if_need()
|
||||
# 如果认证没有过,检查账号密码
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_or_auth(serializer.validated_data)
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
|
@@ -1,15 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from captcha.fields import CaptchaField, CaptchaTextInput
|
||||
|
||||
from common.utils import get_logger, rsa_decrypt_by_session_pkey
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EncryptedField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
return rsa_decrypt_by_session_pkey(value)
|
||||
|
||||
|
||||
class UserLoginForm(forms.Form):
|
||||
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or days_auto_login < 1
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
|
||||
or days_auto_login < 1
|
||||
|
||||
username = forms.CharField(
|
||||
label=_('Username'), max_length=100,
|
||||
@@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
|
||||
'autofocus': 'autofocus'
|
||||
})
|
||||
)
|
||||
password = forms.CharField(
|
||||
password = EncryptedField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=1024, strip=False
|
||||
)
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import gen_key_pair
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
"""
|
||||
@@ -48,3 +52,28 @@ class SessionCookieMiddleware(MiddlewareMixin):
|
||||
return response
|
||||
response.set_cookie(key, value)
|
||||
return response
|
||||
|
||||
|
||||
class EncryptedMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@staticmethod
|
||||
def check_key_pair(request, response):
|
||||
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||
public_key = request.session.get(pub_key_name)
|
||||
cookie_key = request.COOKIES.get(pub_key_name)
|
||||
if public_key and public_key == cookie_key:
|
||||
return
|
||||
|
||||
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key, public_key = gen_key_pair()
|
||||
public_key_decode = base64.b64encode(public_key.encode()).decode()
|
||||
request.session[pub_key_name] = public_key_decode
|
||||
request.session[pri_key_name] = private_key
|
||||
response.set_cookie(pub_key_name, public_key_decode)
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
self.check_key_pair(request, response)
|
||||
return response
|
||||
|
@@ -23,9 +23,7 @@ from acls.models import LoginACL
|
||||
from users.models import User
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -91,46 +89,8 @@ def authenticate(request=None, **credentials):
|
||||
auth.authenticate = authenticate
|
||||
|
||||
|
||||
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 None:
|
||||
return raw_passwd
|
||||
|
||||
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
|
||||
class CommonMixin:
|
||||
request: Request
|
||||
|
||||
def get_request_ip(self):
|
||||
ip = ''
|
||||
@@ -139,26 +99,6 @@ class PasswordEncryptionViewMixin:
|
||||
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 CommonMixin(PasswordEncryptionViewMixin):
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=error)
|
||||
|
||||
@@ -193,20 +133,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
|
||||
user.backend = self.request.session.get("auth_backend")
|
||||
return user
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
def get_auth_data(self, data):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
|
||||
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
|
||||
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
|
||||
ip = self.get_request_ip()
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.get_decrypted_password()
|
||||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
@@ -482,10 +415,10 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
def check_user_auth(self, valid_data=None):
|
||||
# pre check
|
||||
self.check_is_block()
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(valid_data)
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
|
||||
# check auth
|
||||
@@ -537,11 +470,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
self.mark_password_ok(user, False)
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
def get_user_or_auth(self, valid_data):
|
||||
request = self.request
|
||||
if not request.session.get('auth_password'):
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
return self.get_user_from_session()
|
||||
if request.session.get('auth_password'):
|
||||
return self.get_user_from_session()
|
||||
else:
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
|
@@ -2,6 +2,8 @@
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
@@ -10,7 +12,7 @@ __all__ = [
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
password = EncryptedField()
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
|
@@ -161,6 +161,7 @@
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div class="contact-form col-md-10 col-md-offset-1">
|
||||
|
||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
@@ -241,20 +242,10 @@
|
||||
{% include '_foot_js.html' %}
|
||||
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
<script>
|
||||
function encryptLoginPassword(password, rsaPublicKey) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
var jsencrypt = new JSEncrypt(); //加密对象
|
||||
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||
return jsencrypt.encrypt(password); //加密
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
//公钥加密
|
||||
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||
var password = $('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
var passwordEncrypted = encryptPassword(password)
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
@@ -1,62 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import base64
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5
|
||||
from Cryptodome import Random
|
||||
|
||||
from django.conf import settings
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def gen_key_pair():
|
||||
""" 生成加密key
|
||||
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||
"""
|
||||
random_generator = Random.new().read
|
||||
rsa = RSA.generate(1024, random_generator)
|
||||
rsa_private_key = rsa.exportKey().decode()
|
||||
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||
return rsa_private_key, rsa_public_key
|
||||
|
||||
|
||||
def rsa_encrypt(message, rsa_public_key):
|
||||
""" 加密登录密码 """
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||
return cipher_text
|
||||
|
||||
|
||||
def rsa_decrypt(cipher_text, rsa_private_key=None):
|
||||
""" 解密登录密码 """
|
||||
if rsa_private_key is None:
|
||||
# rsa_private_key 为 None,可以能是API请求认证,不需要解密
|
||||
return cipher_text
|
||||
|
||||
key = RSA.importKey(rsa_private_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_decoded = base64.b64decode(cipher_text.encode())
|
||||
# Todo: 弄明白为何要以下这么写,https://xbuba.com/questions/57035263
|
||||
if len(cipher_decoded) == 127:
|
||||
hex_fixed = '00' + cipher_decoded.hex()
|
||||
cipher_decoded = base64.b16decode(hex_fixed.upper())
|
||||
message = cipher.decrypt(cipher_decoded, b'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def check_different_city_login_if_need(user, request):
|
||||
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
|
||||
return
|
||||
|
||||
ip = get_request_ip(request) or '0.0.0.0'
|
||||
|
||||
if not (ip and validate_ip(ip)):
|
||||
city = DEFAULT_CITY
|
||||
else:
|
||||
|
@@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
self.request.session.delete_test_cookie()
|
||||
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
self.check_user_auth(form.cleaned_data)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
@@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_from_session()
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
except (errors.CredentialError, errors.SessionEmptyError) as e:
|
||||
|
Reference in New Issue
Block a user