diff --git a/apps/authentication/backends/ukey/backends.py b/apps/authentication/backends/ukey/backends.py index 3fdd90a47..c937951e5 100644 --- a/apps/authentication/backends/ukey/backends.py +++ b/apps/authentication/backends/ukey/backends.py @@ -48,7 +48,7 @@ class UKeyBackend(JMSBaseAuthBackend): return self._authenticate_sm2(cert_pem, username, signature, challenge, user) else: return self._authenticate_other(cert_pem, username, signature, challenge, user) - except UKeyAuthError as e: + except Exception as e: if request: request.error_message = str(e) raise PermissionDenied(str(e)) @@ -60,10 +60,11 @@ class UKeyBackend(JMSBaseAuthBackend): ukey_sn = (ukey_sn or '').strip() user = User.objects.filter(username=username).first() if user is None: - logger.warning('UKeyBackend: user %r not found', username) + logger.error('UKeyBackend: user %r not found', username) raise UKeyUserNotFoundError() - if user.ukey_sn and ukey_sn != user.ukey_sn: - logger.warning('UKeyBackend: ukey_sn mismatch for user %r', username) + user_ukey_sn = (user.ukey_sn or '').strip() + if not user_ukey_sn or not ukey_sn or ukey_sn != user_ukey_sn: + logger.error('UKeyBackend: ukey_sn mismatch for user %r', username) raise UkeySNMismatchError() return user @@ -91,7 +92,7 @@ class UKeyBackend(JMSBaseAuthBackend): sm2_cert = Sm2Certificate() sm2_cert.import_pem(cert_file) except Exception as e: - logger.warning('UKeyBackend: failed to load SM2 cert: %s', e) + logger.error('UKeyBackend: failed to load SM2 cert: %s', e) raise UKeyCertNormalizationError() finally: if os.path.exists(cert_file): @@ -104,7 +105,7 @@ class UKeyBackend(JMSBaseAuthBackend): try: validity = sm2_cert.get_validity() except Exception as e: - logger.warning('UKeyBackend: failed to get SM2 cert validity: %s', e) + logger.error('UKeyBackend: failed to get SM2 cert validity: %s', e) raise UKeyCertExpiredError() UKeyBackend._check_validity_period(validity.not_before, validity.not_after, 'SM2') @@ -128,14 +129,14 @@ class UKeyBackend(JMSBaseAuthBackend): except UKeyAuthError: raise except Exception as e: - logger.warning('UKeyBackend: SM2 cert chain verification error: %s', e) + logger.error('UKeyBackend: SM2 cert chain verification error: %s', e) raise UKeyCertChainError() finally: if os.path.exists(ca_cert_file): os.unlink(ca_cert_file) if not ok: - logger.warning('UKeyBackend: SM2 cert chain verification failed') + logger.error('UKeyBackend: SM2 cert chain verification failed') raise UKeyCertChainError() @staticmethod @@ -150,10 +151,10 @@ class UKeyBackend(JMSBaseAuthBackend): verifier.update(signed_data) ok = bool(verifier.verify(sig_bytes)) except Exception as e: - logger.warning('UKeyBackend: SM2 signature verification error: %s', e) + logger.error('UKeyBackend: SM2 signature verification error: %s', e) raise UKeySignatureError() if not ok: - logger.warning('UKeyBackend: SM2 signature mismatch') + logger.error('UKeyBackend: SM2 signature mismatch') raise UKeySignatureError() # ── Part 3: RSA / 其他证书校验流程 ─────────────────────────────────────── @@ -176,15 +177,15 @@ class UKeyBackend(JMSBaseAuthBackend): try: cert = x509.load_pem_x509_certificate(cert_pem.encode()) except Exception as e: - logger.warning('UKeyBackend: failed to load certificate: %s', e) + logger.error('UKeyBackend: failed to load certificate: %s', e) raise UKeyCertNormalizationError() pub_key = cert.public_key() if isinstance(pub_key, ec.EllipticCurvePublicKey): - logger.warning('UKeyBackend: ECDSA certificate verification is not supported') + logger.error('UKeyBackend: ECDSA certificate verification is not supported') raise UKeyCertUnsupportedAlgorithmError() if not isinstance(pub_key, rsa.RSAPublicKey): - logger.warning('UKeyBackend: unsupported key type: %s', type(pub_key).__name__) + logger.error('UKeyBackend: unsupported key type: %s', type(pub_key).__name__) raise UKeyCertUnsupportedAlgorithmError() return cert, pub_key @@ -204,7 +205,7 @@ class UKeyBackend(JMSBaseAuthBackend): ca_cert_content = ukey_sdk_config.ca_cert_content if not ca_cert_content: - logger.warning('UKeyBackend: AUTH_UKEY_CA_CERT_CONTENT not configured') + logger.error('UKeyBackend: AUTH_UKEY_CA_CERT_CONTENT not configured') raise UKeyCertChainError() try: ca_cert = x509.load_pem_x509_certificate(ca_cert_content.encode()) @@ -215,12 +216,12 @@ class UKeyBackend(JMSBaseAuthBackend): cert.signature_hash_algorithm, ) except InvalidSignature: - logger.warning('UKeyBackend: RSA cert chain verification failed') + logger.error('UKeyBackend: RSA cert chain verification failed') raise UKeyCertChainError() except UKeyAuthError: raise except Exception as e: - logger.warning('UKeyBackend: RSA cert chain verification error: %s', e) + logger.error('UKeyBackend: RSA cert chain verification error: %s', e) raise UKeyCertChainError() @staticmethod @@ -245,12 +246,12 @@ class UKeyBackend(JMSBaseAuthBackend): try: pub_key.verify(sig_bytes, signed_data, padding.PKCS1v15(), hashes.SHA256()) except InvalidSignature: - logger.warning('UKeyBackend: RSA signature mismatch') + logger.error('UKeyBackend: RSA signature mismatch') raise UKeySignatureError() except UKeyAuthError: raise except Exception as e: - logger.warning('UKeyBackend: RSA signature verification error: %s', e) + logger.error('UKeyBackend: RSA signature verification error: %s', e) raise UKeySignatureError() # ── 公共工具方法 ────────────────────────────────────────────────────────── @@ -270,12 +271,12 @@ class UKeyBackend(JMSBaseAuthBackend): now = datetime.datetime.now() if now < not_before: - logger.warning( + logger.error( 'UKeyBackend: %s certificate not yet valid, valid from %s', label, not_before ) raise UKeyCertExpiredError() if now > not_after: - logger.warning( + logger.error( 'UKeyBackend: %s certificate has expired at %s', label, not_after ) raise UKeyCertExpiredError() @@ -284,7 +285,7 @@ class UKeyBackend(JMSBaseAuthBackend): def _verify_cert_cn(cert_cn, username): """校验证书 CN 与 username 是否匹配(SM2 和 RSA 流程共用)。""" if cert_cn != username: - logger.warning( + logger.error( 'UKeyBackend: cert CN %r does not match username %r', cert_cn, username ) raise UKeyCertCNMismatchError() @@ -300,7 +301,7 @@ class UKeyBackend(JMSBaseAuthBackend): try: return UKeyBackend._normalize_cert_to_pem(cert_data) except Exception as e: - logger.warning('UKeyBackend: cert normalization failed: %s', e) + logger.error('UKeyBackend: cert normalization failed: %s', e) raise UKeyCertNormalizationError() @staticmethod diff --git a/apps/authentication/backends/ukey/forms.py b/apps/authentication/backends/ukey/forms.py index 6054be53e..deffbc22d 100644 --- a/apps/authentication/backends/ukey/forms.py +++ b/apps/authentication/backends/ukey/forms.py @@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _ class UKeyLoginForm(forms.Form): username = forms.CharField( - label=_('Username'), max_length=100, required=True, + required=True, + label=_('Username'), widget=forms.HiddenInput(), ) cert = forms.CharField( @@ -16,7 +17,7 @@ class UKeyLoginForm(forms.Form): widget=forms.HiddenInput(), ) ukey_sn = forms.CharField( - required=False, + required=True, widget=forms.HiddenInput(), ) diff --git a/apps/authentication/backends/ukey/views.py b/apps/authentication/backends/ukey/views.py index 995fb67d3..23048f70c 100644 --- a/apps/authentication/backends/ukey/views.py +++ b/apps/authentication/backends/ukey/views.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # import secrets +from urllib.parse import urlencode from django.conf import settings from django.contrib.auth import authenticate +from django.contrib import messages from django.core.cache import cache from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ @@ -25,7 +27,6 @@ from .sdk import ukey_sdk_config __all__ = ['UKeyLoginView'] _CHALLENGE_CACHE_KEY_PREFIX = 'ukey_login_challenge' -NEXT_URL = 'next' @method_decorator(sensitive_post_parameters(), name='dispatch') @method_decorator(csrf_protect, name='dispatch') @@ -59,28 +60,37 @@ class UKeyLoginView(AuthMixin, FormView): def _delete_stored_challenge(self): cache.delete(self._challenge_cache_key()) + def _get_next_url(self): + next = self.request.GET.get(self.redirect_field_name) + next = next or self.request.POST.get(self.redirect_field_name) + return next + + def _build_login_redirect_url(self): + next_url = self._get_next_url() + if not next_url: + return self.request.path + query = urlencode({self.redirect_field_name: next_url}) + return f'{self.request.path}?{query}' + # ------------------------------------------------------------------ # Views # ------------------------------------------------------------------ - def get(self, request, *args, **kwargs): - challenge = self._generate_and_store_challenge() - context = self.get_context_data(form=self.get_form(), challenge=challenge) - return self.render_to_response(context) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if 'challenge' not in context: - context['challenge'] = self._get_stored_challenge() + context['challenge'] = self._generate_and_store_challenge() return context def form_valid(self, form): username = form.cleaned_data['username'] cert = form.cleaned_data['cert'] signature = form.cleaned_data['signature'] - ukey_sn = form.cleaned_data.get('ukey_sn', '').strip() + ukey_sn = form.cleaned_data['ukey_sn'] challenge = self._get_stored_challenge() + if not challenge: + error = _('Authentication challenge expired, please refresh the page and try again.') + return self.get_failed_response(form, username, error) error_msg = None ip = self.get_request_ip() @@ -115,14 +125,39 @@ class UKeyLoginView(AuthMixin, FormView): return self.get_failed_response(form, username, error_msg) else: return self.get_success_response(self.request, user) - + + def form_invalid(self, form): + error_msg = self._get_form_error_message(form) + username = (form.data.get('username') or '').strip() + return self.get_failed_response(form, username, error_msg) + + @staticmethod + def _get_form_error_message(form): + non_field_errors = list(form.non_field_errors()) + if non_field_errors: + return ' '.join(non_field_errors) + + field_errors = [] + for field_name, errors in form.errors.items(): + if field_name == '__all__': + continue + field_label = UKeyLoginView._get_field_label(form, field_name) + field_errors.append(f"{field_label}: {' '.join(errors)}") + if field_errors: + return ' '.join(field_errors) + return _('Unknown') + + @staticmethod + def _get_field_label(form, field_name): + field = form.fields.get(field_name) + if field and field.label: + return field.label + return field_name + def get_failed_response(self, form, username, error_msg): - form.add_error(None, error_msg) - # Refresh the challenge so it cannot be replayed - challenge = self._generate_and_store_challenge() - context = self.get_context_data(form=form, challenge=challenge) + messages.error(self.request, error_msg) self.send_auth_signal(success=False, reason=error_msg, username=username) - return self.render_to_response(context) + return redirect(self._build_login_redirect_url()) def get_success_response(self, request, user): self.mark_ukey_ok(user, auth_backend=settings.AUTH_BACKEND_UKEY) diff --git a/apps/authentication/templates/authentication/login_ukey.html b/apps/authentication/templates/authentication/login_ukey.html index 703ff7bf9..bb21be15e 100644 --- a/apps/authentication/templates/authentication/login_ukey.html +++ b/apps/authentication/templates/authentication/login_ukey.html @@ -202,36 +202,26 @@
{% csrf_token %} - {% if form.non_field_errors %} + {% if messages %}
-

{{ form.non_field_errors.as_text }}

+ {% for message in messages %} +

{% trans "Error" %}: {{ message }}

+ {% endfor %}
{% endif %} - {# username is auto-filled from UKey cert, display input is readonly, actual value submitted via hidden field #} -
+ {# username is auto-filled from UKey cert, display input is readonly #} +
- {{ form.username }} - {% if form.username.errors %} -

{{ form.username.errors.as_text }}

- {% endif %}
{# PIN is only used for client-side UKey verification, not submitted to backend #}
- {# ukey cert is fetched and filled by JS after PIN verification, not shown to user #} - {{ form.cert }} - {# signature is filled by JS after signing challenge code, not shown to user #} - {{ form.signature }} - {# ukey_sn is filled by JS after detecting cert, not shown to user #} - {{ form.ukey_sn }} - -