Django a Ajax:Robustní autentizační a autorizační systém s ověřováním formulářů v reálném čase pro webové aplikace - 2

V předchozí části jsme navrhli schéma databáze tak, aby odpovídalo této části specifikace:

Zdrojový kód

Zdrojový kód je k tomuto bodu hostován na githubu, zatímco zdrojový kód celé aplikace je:

django_real_time_validation

Django a Ajax:Robustní systém ověřování a autorizace s ověřováním formulářů pro webové aplikace v reálném čase


Zobrazit na GitHubu

Projekt je také živě na heroku a je přístupný přes tuto django-authentication-app.herokuapp.com

V této části se podíváme na to, jak bude logika implementována. Nějaká část views.py , urls.py , forms.py a authentication.py bude implementováno.

Nasaďme si náš kódovací hart 👲 a ušpiněme si ruce 🧰!

Krok 2:Vytvoření dalších souborů

Nejprve budeme používat další soubory následovně:

  • accounts/forms.py :toto obsahuje vše, co souvisí s formou.
  • accounts/utils.py :abyste se vyhnuli nepřehlednému views.py soubor, pomocné funkce budou umístěny zde.
  • accounts/authentication.py :zde se nachází vlastní autentizační backend, který budeme používat k povolení přihlášení pomocí e-mailové adresy i uživatelského jména.

Chcete-li vytvořit soubory, přejděte do svého terminálu a spusťte následující příkaz:

┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch accounts/utils.py accounts/forms.py accounts/authentication.py

Krok 3:Vlastní autentizační backend

Část specifikace, kterou implementujeme, říká:

K tomu potřebujeme vlastní autentizační backend. Naštěstí nám django ukazuje, jak to lze udělat. Spusťte textový editor a vytvořte accounts/authentication.py vypadat takto:

# accounts > authentication.py

from .models import User


class EmailAuthenticationBackend(object):
    """
    Authenticate using an e-mail address.
    """

    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):  # and user.is_active:
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Nedědíme zde žádný vestavěný backend, ale stále to funguje. Stále se však vracíme k výchozímu autentizačnímu backendu Django, který se ověřuje pomocí uživatelského jména.

Přestože jsme napsali tento samovysvětlující úryvek kódu, zatím nic nedělá. Aby to něco dělalo, musíme to zaregistrovat. Připojte níže uvedený úryvek k settings.py vašeho projektu soubor:

# authentication > settings.py
...
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.EmailAuthenticationBackend", # our new authentication backend
]
...

Pojďme přidat náš nový User model na stránku správce django. Otevřete accounts/admin.py a připojte následující:

# accounts > admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User


class CustomUserAdmin(UserAdmin):
    model = User
    readonly_fields = [
        "date_joined",
    ]
    actions = [
        "activate_users",
    ]
    list_display = (
        "username",
        "email",
        "first_name",
        "last_name",
        "is_staff",
        "is_student",
        "is_lecturer",
    )

    def get_inline_instances(self, request, obj=None):
        if not obj:
            return list()
        return super(CustomUserAdmin, self).get_inline_instances(request, obj)

    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser
        disabled_fields = set()

        if not is_superuser:
            disabled_fields |= {
                "username",
                "is_superuser",
            }
        # Prevent non-superusers from editing their own permissions
        if not is_superuser and obj is not None and obj == request.user:
            disabled_fields |= {
                "is_staff",
                "is_superuser",
                "groups",
                "user_permissions",
            }
        for f in disabled_fields:
            if f in form.base_fields:
                form.base_fields[f].disabled = True

        return form

    def activate_users(self, request, queryset):
        cannot = queryset.filter(is_active=False).update(is_active=True)
        self.message_user(request, "Activated {} users.".format(cannot))

    activate_users.short_description = "Activate Users"  # type: ignore

    def get_actions(self, request):
        actions = super().get_actions(request)
        if not request.user.has_perm("auth.change_user"):
            del actions["activate_users"]
        return actions


admin.site.register(User, CustomUserAdmin)

Nastavili jsme vlastní uživatelskou administrační obchodní logiku. Do kódu jsme přidali vlastní akci activate user který umožňuje aktivaci velkého počtu uživatelů najednou. To bylo implementováno v případě, že selže registrační tok, který plánujeme, a chceme, aby superuživatel měl možnost hromadně aktivovat uživatele. Také skryjeme několik polí všem uživatelům, kteří mají přístup na stránku správce, ale nemají superuser . To je z bezpečnostních důvodů. Chcete-li se o tom dozvědět více, článek Haki Benity je skvělým průvodcem.

Krok 4:Logika zobrazení přihlášení

Je čas otestovat naše custom authentication backend. Nejprve potřebujeme formulář pro přihlášení uživatelů. Pojďme to vytvořit.

# accounts > forms.py

from django import forms


class LoginForm(forms.Form):
    username = forms.CharField(widget=forms.TextInput(attrs={"placeholder": "Username or Email"}))
    password = forms.CharField(widget=forms.PasswordInput(attrs={"placeholder": "Password"}))

    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        for visible in self.visible_fields():
            visible.field.widget.attrs["class"] = "validate"

Je to velmi jednoduchý formulář se dvěma poli:username a password . Nicméně username pole také pojme email adresy. To je v souladu s naší specifikací. __init__ metoda dunder platí class=validate do všech viditelných polí ve formuláři. Je to hezká zkratka hlavně při práci s ModelForms . Toto validate třída je k dispozici v materialize css . Dalším agentem je použití tohoto formuláře v views.py soubor.

# accounts > views.py

from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render
from django.urls.base import reverse

from .forms import LoginForm

...

def login_user(request):
    form = LoginForm(request.POST or None)
    msg = "Enter your credentials"
    if request.method == "POST":
        if form.is_valid():
            username = form.cleaned_data.get("username").replace("/", "")
            password = form.cleaned_data.get("password")
            user = authenticate(username=username, password=password)
            if user is not None:
                if user.is_active:
                    login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
                    messages.success(request, f"Login successful!")
                    if "next" in request.POST:
                        return redirect(request.POST.get("next"))
                    else:
                        return redirect("accounts:index")
                else:
                    messages.error(
                        request,
                        f"Login unsuccessful! Your account has not been activated. Activate your account via {reverse('accounts:resend_email')}",
                    )
                    msg = "Inactive account details"
            else:
                messages.error(request, f"No user with the provided details exists in our system.")
        else:
            messages.error(request, f"Error validating the form")
            msg = "Error validating the form"
    context = {
        "form": form,
        "page_title": "Login in",
        "msg": msg,
    }
    return render(request, "accounts/login.html", context)

Je to základní autentizační logika. Některé ukazatele odstraňují všechna lomítka / ze zadaného uživatelského jména, v případě studentů, a pomocí našeho vlastního autentizačního backendu:

...
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
...

pro přihlášení uživatelů. Zabývali jsme se také částí specifikace, která říká:

Ve výchozím nastavení se však nemůžete přihlásit, pokud je is_active=False ale protože používáme vlastní authentication backend, mám pocit, že bychom to měli prosadit. Mohli jsme to udělat dříve v authentication backendový kód. Dále zkontrolujeme, zda existuje stránka, na kterou potřebujeme přesměrovat, kontrolou obsahu next . Brzy to vložíme do naší šablony. Je to pěkný způsob, jak přesměrovat uživatele zpět tam, kam chtěli navštívit, než budou požádáni o přihlášení.

Přidejme toto a django vestavěný logout podívejte se na naše urls.py soubor.

# accounts > urls.py

from django.contrib.auth import views as auth_views
...

urlpatterns = [
    ...

    path("login", views.login_user, name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]

Rozšíření, pojďme to zaregistrovat v našem settings.py soubor také.

# accounts > settings.py

...

AUTH_USER_MODEL = "accounts.User"
LOGIN_URL = "accounts:login"
LOGOUT_URL = "accounts:logout"
LOGOUT_REDIRECT_URL = "accounts:index"

...

Vždy se chceme vrátit na domovskou stránku, když se odhlásíme.

Konečně je čas to vykreslit.

{% extends "base.html" %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock %}
<!--content-->
{% block content%}
<h4 id="signup-text">Welcome back</h4>
<div class="form-container">
  <!--  <h5 class="auth-header">Assignment Management System</h5>-->
  <div class="signin-form">
    <form method="POST" action="" id="loginForm">
      {% csrf_token %}
      <!---->
      <h5 style="text-align: ceneter">{{msg}}</h5>
      <div class="row">
        {% for field in form %}
        <div class="input-field col s12">
          {% if forloop.counter == 1 %}
          <i class="material-icons prefix">email</i>
          {% elif forloop.counter == 2 %}
          <i class="material-icons prefix">vpn_key</i>
          {% endif %}
          <label for="id_{{field.label|lower}}"> {{field.label}}* </label>
          {{ field }}
          <!---->
          {% if field.errors %}
          <span class="helper-text email-error">{{field.errors}}</span>
          {% endif %}
        </div>
        {% endfor %}
      </div>

      <!---->
      {% if request.GET.next %}
      <input type="hidden" name="next" value="{{request.GET.next}}" />
      {% endif %}
      <button
        class="btn waves-effect waves-light btn-large"
        type="submit"
        name="login"
        id="loginBtn"
      >
        Log in
        <i class="material-icons right">send</i>
      </button>
    </form>
    <ul>
      <li class="forgot-password-link">
        <a href="#"> Forgot password?</a>
      </li>
    </ul>
  </div>
  <div class="signup-illustration">
    <img
      src="{% static 'img/sign-up-illustration.svg' %}"
      alt="Sign in illustration"
    />
  </div>
</div>

{% endblock %}

Jedná se o základní materializovaný css formulář s ikonami. Protože máme pouze dvě pole, username/email a password , používáme if pro kontrolu forloop čítač a vložte icons vhodně. Všimli jste si tohoto řádku?:

 {% if request.GET.next %}
      <input type="hidden" name="next" value="{{request.GET.next}}" />
 {% endif %}

To je to, co ušetří next pole, o kterém jsme hovořili dříve. Je to skrytý vstup, protože nechceme, aby uživatelé viděli jeho obsah, jen pro referenci.

Abychom zahájili ověřování formulářů v reálném čase, po kterém jsme se dožadovali, přidejte do tohoto formuláře trochu JavaScriptu. Nejprve chceme Log in deaktivovat, dokud uživatelé nezadají obě username or email a password . To zatím stačí.

Připojte tento kód k templates/accounts/login.html soubor:

<!---->
{% block js %}
<script>
  const loginForm = document.getElementById("loginForm");
  const formElements = document.querySelectorAll("#loginForm  input");
  loginForm.addEventListener("keyup", (event) => {
    let empty = false;
    formElements.forEach((element) => {
      if (element.value === "") {
        empty = true;
      }
    });

    if (empty) {
      $("#loginBtn").addClass("disabled");
    } else {
      $("#loginBtn").removeClass("disabled");
    }
  });
</script>
{% endblock js %}

Jednoduše poslouchá keyup události v libovolném vstupním prvku formuláře. Pokud je některé prázdné, tlačítko zůstane neaktivní, jinak? Povoleno! Jednoduché co 😎!

Upravte tlačítko tak, aby bylo ve výchozím nastavení zakázáno.

...

<button class="btn waves-effect waves-light btn-large disabled"
        type="submit"
        name="login"
        id="loginBtn"
      >
        Log in
        <i class="material-icons right">send</i>
      </button>

...

Již jsme vytvořili js blok ve spodní části templates/base.html soubor

Nyní aktualizujte templates/includes/_header.html takže můžeme mít snadnou navigaci pro mobilní i stolní části.

...

<li><a href="{% url 'accounts:logout' %}">Logout</a></li>

...

 <li><a href="{% url 'accounts:login' %}">Login</a></li>

...

Můžeme to teď otestovat? Protože se nemůžu dočkat 💃🕺.

Sakra! Je to přitažlivé 🤗... Vytvořte si superuživatelský účet a vyzkoušejte jej buď s Email or username a password .

Chcete kód pro tento účel? Získejte to na github

Tady to ukončíme, začíná to být neúnosně dlouhé 😌. Uvidíme se 👋 🚶!!!

Outro

Tento článek se vám líbil, zvažte, zda mě neoslovíte kvůli práci, něčemu, co stojí za to nebo si koupíte kávu ☕.