Tạo xác thực người dùng bằng Django Authentication

Like bài viết:1 like

Trong hướng dẫn này, mình sẽ trình bày cách để tạo xác thực người dùng trong Django sử dụng Django Authentication. Bao gồm tạo trang đăng nhập, đăng ký, cập nhật tài khoản, thay đổi mật khẩu ....

Bên cạnh đó cũng hướng dẫn các để mở rộng thêm các thuộc tính cho người dùng.

Khởi tạo project

Project

Chúng ta bắt đầu cài Django và khởi tạo project:

pip install django==3.0.3
django-admin startproject myproject .
Lưu ýCó dấu chấm phía cuối dòng lệnh 

Users App

Tạo một ứng dụng để quản lý người dùng:

python manage.py startapp users

Thêm ứng dụng vừa tạo vào project:

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    'users',
]

 

Django Authentication

Django Auth App

Django cung cấp sẵn cho bạn một ứng dụng để giúp xử lý với người dùng là django.contrib.auth. Về cơ bản, nó là một app như các app mà bạn tạo bao gồm views, forms, các models hay urls.

Theo mặc định, app này sẽ tạo cho người dùng một số trường (fields) nhất định như first_name, last_name, username, email, password, is_staff, is_active... ( xem thêm )

Chúng ta sẽ tiến hành tạo Model cho người dùng, bằng cách:

users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    pass

Django Authentication cung cấp cho bạn rất nhiều models để sử dụng. Trong đó, bạn chỉ cần chú ý đến 3 models:

  • AbstractBaseUser: Gồm các thuộc tính rất cơ bản gồm password, last_login, is_active... 
  • AbstractUser: Kế thừa từ AbstractBaseUser, tuy nhiên có thêm nhiều thuộc tính khác như first_name, last_name, username, email...
  • User: Là kết thừa từ AbstractUser. Django theo mặc định sẽ sử dụng Model này để làm model người dùng.

Như ở trên, mình kết thừa lại từ AbstractUser mà không thêm bất cứ thuộc tính nào khác. Tuy nhiên, trong thực tế thì việc mở rộng thuộc tính người dùng là không tránh khỏi. Ví dụ mình sẽ thêm một số thuộc tính khác cho người dùng:

users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    address = models.CharField(max_length=100, blank=True)
    photo = models.ImageField(upload_to="profile/", blank=True)
    description = models.TextField(blank=True)

Ở đây, mình thêm 3 thuộc tính khác gồm: 

  • address: Địa chỉ của người dùng
  • photo: Ảnh đại diện. Bạn bắt buộc phải cài đặt Pillow nếu sử dụng trường ImageField. Tiến hành cài đặt như sau:
    pip install Pillow==2.2.1
  • description: Miêu tả, giới thiệu bản thân. Mình để là TextField

Một bước cuối cùng cần thực hiện là bạn cần bảo với Django rằng bạn đã thay đổi Model của người dùng ( Vì theo mặc định Django sử dụng model là User). Bạn thực hiện cài  CustomUser làm model cho người dùng mặc định như sau:

myproject/settings.py

AUTH_USER_MODEL = 'users.CustomUser'

Bây giờ tiến hành chạy migrations:

python manage.py makemigrations users
python manage.py migrate

 

Home Page

Đầu tiên mình sẽ tạo trang home.

myproject/urls.py

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
]

Template

Xác định thư mục template sẽ sử dụng:

myproject/settings.py

TEMPLATES = [
    {
        ...
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        ...
    },
]

Tiến hành tạo file base.html.

templates/base.html

<!doctype html>
{% load static %}
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{% block title %}{% endblock title %}</title>
    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"\
      integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom box-shadow">
            <a href="{% url 'home' %}" class="navbar-brand my-0 mr-md-auto font-weight-normal">
                djangobat
            </a>
            <nav class="my-2 my-md-0 mr-md-3">
                {% if user.is_authenticated %}
                    <a class="p-2 text-dark" href="">Hi! {{ user.username }}</a>
                    <a class="btn btn-danger mr-2" href="">Đăng xuất</a>
                {% else %}
                <div class="d-flex">
                    <a class="btn btn-outline-success mr-2" href="">Đăng ký</a>
                    <a class="btn btn-primary mr-2" href="">Đăng nhập</a>
                </div>
                {% endif %}
            </nav>
        </div>
    </header>
    <div class="container p-5">
        {% block main %}
        {% endblock main %}
    </div>
</body>
</html>

Bạn chú ý rằng Django luôn cung cấp cho bạn một biến người dùng bạn có thể truy cập ở bất cứ templatenào với biến là user. Và ở đây mình sử dụng gọi user.is_authenticated để xem người dùng này đã được xác thực chưa ( đã đăng nhập chưa), nếu chưa sẽ hiện nút đăng kýđăng nhập, còn nếu đã đăng nhập sẽ Hi! và nút đăng xuất.

templates/home.html

{% extends "base.html" %}
{% block title %}
Home Page
{% endblock title %}
{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">djangobat</h1>
    <p class="lead text-muted">Tạo trang xác thực người dùng với Django</p>
</div>
{% endblock main %}

Chạy server :

python manage.py runserver

Kết quả:

 

URLs

Thêm URLs của Django Auth vào project:

myproject/urls.py

from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TemplateView.as_view(template_name='home.html'), name='home'),

    path('users/', include('django.contrib.auth.urls')),
]

Django Auth cung cấp cho bạn các tên URLs như sau:

urlpatterns = [
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),

    path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),

    path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

Vì bạn đã thêm vào dự án nên bây giờ bạn có thể sử dụng các urls trên. Bắt đầu bằng việc tạo trang đăng ký.

Login

Sửa lại file base.html đã tạo trước đó để thêm Login:

templates/base.html

...
<a class="btn btn-primary mr-2" href="{% url 'login' %}">Đăng nhập</a>
...

Template

Theo mặc định Django Auth sẽ tìm kiếm các file có trong thư mục registration bên trong thư mục templates làm thư mục chứa các templates.

Với trang Login, bạn sẽ phải tạo một file tên login.html bên trong thư mục registration:

templates/registration/login.html

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}
    Đăng nhập
{% endblock title %}

{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Đăng nhập</h1>
    <p class="lead text-muted">Nhập thông tin phía dưới để tiến hành đăng nhập.</p>
</div>       
<form action="{% url 'login' %}" method="post" style="max-width: 420px; margin: auto;">
    {% csrf_token %}

    {{ form.as_p }}
    
    <button class="btn btn-lg btn-primary" type="submit">Đăng nhập</button>
    <p class="mt-3">
        Nếu chưa có tài khoản <a href="">đăng ký</a>
    </p>
</form>
{% endblock main %}
  • csrf_token : Để tránh bị tấn công cross-site request forgery (CSRF)
  • form: Đây là biến được truyền tới theo mặc định, trang login sẽ gồm 2 trường là usernamepassword

Mở trang 127.0.0.1:8000/users/login/ ta thấy:

Django-widget-tweaks

Tiến hành cài đặt django-widget-tweaks để sử dụng tùy chỉnh các class cho bootstrap:

pip install django-widget-tweaks==1.4.5

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'widget_tweaks',
    
    'users',
]

templates/registration/login.html

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}
    Đăng nhập
{% endblock title %}

{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Đăng nhập</h1>
    <p class="lead text-muted">Nhập thông tin phía dưới để tiến hành đăng nhập.</p>
</div>       
<form action="{% url 'login' %}" method="post" style="max-width: 420px; margin: auto;">
    {% csrf_token %}

    <div class="form-group">
        <label for="">Username</label>
        {% render_field form.username class="form-control" placeholder="username" %}
    </div>
    <div class="form-group">
        <label for="">Password</label>
        {% render_field form.password class="form-control" placeholder="password" %}
    </div>
    <button class="btn btn-lg btn-primary" type="submit">Đăng nhập</button>
    <p class="mt-3">
        Nếu chưa có tài khoản <a href="">đăng ký</a>
    </p>
</form>
{% endblock main %}

Mở trang 127.0.0.1:8000/users/login/ kết quả:

Tiến hành tạo một tài khoản bằng cách:

$ python manage.py createsuperuser --username test_user --email test_user@gmail.com
$ password: ******

Nhập password của bạn vào. Sau đó dùng tài khoản này để truy cập 127.0.0.1:8000/users/login/ bạn sẽ thấy kết quả Lỗi!

Vì theo mặc định của Django Auth sau khi đăng nhập thành công nó sẽ chuyển hướng tới /accounts/profile/, mà bạn chưa tạo đường dẫn nào như thế cả. Để thay đổi điều này, bạn thêm vào file settings.py

myproject/settings.py

LOGIN_REDIRECT_URL = 'home'

Như thế khi đăng nhập thành công nó sẽ chuyển hướng tới trang chủ.

Logout

Sửa lại file base.html đã tạo trước đó để thêm Logout:

...
<a class="btn btn-danger mr-2" href="{% url 'logout' %}">Đăng xuất</a>
...

Nếu click vào nút logout bạn sẽ được điều hướng tới trang như sau:

Để thay đổi điều này, bạn có 2 cách:

Cách 1: Xác định trang bạn muốn chuyển tới sang khi đăng xuất bằng biến LOGOUT_REDIRECT_URL

myproject/settings.py

LOGOUT_REDIRECT_URL = 'home'

Khi đó sau khi đăng xuất nó sẽ chuyển hướng tới trang chủ

Cách 2: Tiến hành tạo file logged_out.html

templates/registration/logged_out.html

{% extends "base.html" %}
{% block title %}
    Đăng xuất
{% endblock title %}

{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Đăng xuất</h1>
    <p class="lead text-muted">Bạn đã đăng xuất tài khoản, <a href="{% url 'login' %}">Đăng nhập</a> lại hoặc trở về <a href="{% url 'home' %}">trang chủ</a></p>
</div>
{% endblock main %}

Như thế sau khi đăng xuất nó sẽ hiện thị trang này.

 

SignUp

Đối với trang đăng ký thì Django Auth không cung cấp sẵn vì thế chúng ta sẽ phải tự tạo.

Forms

Tạo file forms.py và thực hiện như sau:

users/forms.py

from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm


class SignUpForm(UserCreationForm):
    class Meta:
        model = get_user_model()
        fields = ('username', 'email',)

URLs

users/urls.py

from django.urls import path

from . import views


urlpatterns = [
    path('signup/', views.signup, name='signup'),
]

myproject/urls.py

from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView


urlpatterns = [
    ...
    path('users/', include('users.urls')),
    path('users/', include('django.contrib.auth.urls')),
]

Views

from django.shortcuts import render, redirect

from .forms import SignUpForm


def signup(request):
    if request.method == "POST":
        form = SignUpForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('login')
    else:
        form = SignUpForm()

    return render(request, 'users/signup.html', {'form': form})

Sử dụng redirect để sau khi người dùng đăng ký thành công sẽ chuyển hướng tới trang đăng nhập

Template

Cập nhật lại file base.html

templates/base.html

...
<a class="btn btn-outline-success mr-2" href="{% url 'signup' %}">Đăng ký</a>
...

templates/users/signup.html

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}
    Đăng ký
{% endblock title %}

{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Đăng ký</h1>
    <p class="lead text-muted">Nhập thông tin phía dưới đăng ký tài khoản mới.</p>
</div>
<form action="{% url 'signup' %}" method="post" style="max-width: 420px; margin: auto;">
    {% csrf_token %}

    <div class="form-group">
        <label for="">Username</label>
        {% render_field form.username class="form-control" placeholder="username" %}
    </div>
    <div class="form-group">
        <label for="">Email</label>
        {% render_field form.email class="form-control" placeholder="name@gmail.com" %}
    </div>
    <div class="form-group">
        <label for="">Mật khẩu</label>
        {% render_field form.password1 class="form-control" %}
    </div>
    <div class="form-group">
        <label for="">Nhập lại mật khẩu</label>
        {% render_field form.password2 class="form-control" %}
    </div>
    <button class="btn btn-lg btn-success" type="submit">Đăng ký</button>
    <p class="mt-3">
        Nếu đã tài khoản <a href="{% url 'login' %}">đăng nhập</a>
    </p>
</form>
{% endblock main %}

Mở trang 127.0.0.1:8000/users/signup/ thực hiện đăng ký:

 

Profile

Tiếp theo chúng ta sẽ tiến hành tạo trang để người dùng cập nhật thông tin cá nhân

Form

from django import forms


class CustomUserUpdateForm(forms.ModelForm):
    class Meta:
        model = get_user_model()
        fields = ('first_name', 'last_name', 'email',
                    'address', 'description', 'photo',
                    'username',
                )

Đây là những trường mà chúng ta sẽ cho người dùng tự cập nhật

URLs

from django.urls import path

from . import views


urlpatterns = [
    path('profile/', views.profile, name='profile'),
    path('signup/', views.signup, name='signup'),
]

Views

users/views.py

from django.contrib.auth.decorators import login_required

from .forms import CustomUserUpdateForm


@login_required
def profile(request):
    user = request.user
    if request.method == 'POST': 
        form = CustomUserUpdateForm(request.POST, instance=user, files=request.FILES)
        if form.is_valid():
            form.save()
    else:
        form = CustomUserUpdateForm(instance=user)

    return render(request, 'users/profile.html', {'form': form})
  • login_required: Đây là một decorator mà Django cung cấp để chỉ những người dùng nào đã đăng nhập mới có thể truy cập được view này.
  • files: Vì ở form này chúng ta có cho người dùng upload ảnh lên nên phải thêm files=request.FILES để lấy file đó.

Template

Chỉnh lại file base.html 

templates/base.html

...
<a class="p-2 text-dark" href="{% url 'profile' %}">Hi! {{ user.username }}</a>
...

templates/users/profile.html

{% extends "base.html" %}
{% load static %}
{% load widget_tweaks %}
{% block title %}
Profile
{% endblock title %}
{% block main %}
<div class="text-center pt-5 mb-5">
    <h1 class="display-4">Cập nhật thông tin</h1>
    <p class="lead text-muted">Cập nhật thông tin cá nhân</p>
</div>
<div class="row">
    <div class="col-md-4 mb-4">
        <div class="card">
            {% if user.photo %}
                <img class="card-img-top" src="{{ user.photo.url }}" alt="image" style="width:100%">
            {% else %}
                <img class="card-img-top" src="{% static 'img/avatar.png' %}" alt="image" style="width:100%">
            {% endif %}
            <div class="card-body">
                <h4 class="card-title">{{ user.username }}</h4>
                <p class="card-text">{{ user.description|truncatewords:30 }}</p>
                <a href="{% url 'password_change' %}" class="btn btn-info">Thay đổi mật khẩu</a>
            </div>
        </div>
    </div>
    <div class="col-md-8">
        <form method="post" action="." enctype="multipart/form-data">
            {% csrf_token %}

            <div class="row">
                <div class="col-md-6 mb-3">
                    <label>Họ</label>
                    {% render_field form.first_name class="form-control" %}
                </div>
                <div class="col-md-6 mb-3">
                    <label>Tên</label>
                    {% render_field form.last_name class="form-control" %}
                </div>
            </div>
            <div class="form-group">
                {{ form.photo }}
            </div>
            <div class="row">
                <div class="col-md-6 mb-3">
                    <label>Username</label>
                    {% render_field form.username class="form-control" %}
                </div>
                <div class="col-md-6 mb-3">
                    <label>Email</label>
                    {% render_field form.email class="form-control" %}
                </div>
            </div>
            <div class="mb-3">
                <label>Địa chỉ</label>
                {% render_field form.address class="form-control" %}
            </div>
            <div class="mb-3">
                <label>Giới thiệu bản thân</label>
                {% render_field form.description class="form-control" rows="7" %}
            </div>
            <hr class="mb-4">
            <button class="btn btn-primary btn-lg btn-block" type="submit">Cập nhật</button>
        </form>
    </div>
</div>
{% endblock main %}

Lưu ý là bạn cần thêm enctype="multipart/form-data" vào trong form vì ở trong form bạn có xử lý cả file image.

MEDIA FILES

Để giúp cho người dùng có thể tải files lên chúng ta cần tùy chỉnh thêm một chút. Vì lý do bảo mật, Django không phục vụ các file người dùng tải lên trong môi trường sản xuất. Tuy nhiên, trong môi trường phát triển, chúng ta có thể thực hiện bằng cách:

myproject/settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

myproject/urls.py

...
from django.conf import settings

if settings.DEBUG:
    from django.conf.urls.static import static

    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Mở trang 127.0.0.1:8000/users/profile/ tiến hành cập nhật thông tin:

 

Change Password

Template

Theo mặc định, Django Auth sẽ sử dụng file password_change.html trong thư mục registration. Chúng ta tiến hành tạo và sửa file như sau:

templates/registration/password_change.html

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}
    Thay đổi mật khẩu
{% endblock title %}

{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Thay đổi mật khẩu</h1>
    <p class="lead text-muted">Nhập thông tin phía dưới để tiến hành thay đổi mật khẩu.</p>
</div>
<form action="{% url 'password_change' %}" method="post" style="max-width: 420px; margin: auto;">
    {% csrf_token %}
    
    <div class="form-group">
        <label for="">Mật khẩu hiện tại</label>
        {% render_field form.old_password class="form-control"%}
    </div>
    <div class="form-group">
        <label for="">Mật khẩu mới</label>
        {% render_field form.new_password1 class="form-control" %}
    </div>
    <div class="form-group">
        <label for="">Nhập lại mật khẩu mới</label>
        {% render_field form.new_password2 class="form-control" %}
    </div>
    <button class="btn btn-lg btn-primary" type="submit">Cập nhật mật khẩu</button>
</form>
{% endblock main %}

Vào trang 127.0.0.1:8000/users/password_change/ tiến hành thay đổi mật khẩu :

Xử lý Errors

Ở trên chúng ta đã thực hiện rất nhiều forms nhưng chúng ta đã bỏ qua một phần khá quan trọng là xử lý lỗi.

Lỗi Form trong Django bạn có thể lấy bằng 3 cách:

  • Lỗi riêng : Là lỗi của từng trường riêng biệt. Ví dụ như trường username trong phần Form Login lỗi chúng ta sẽ truy cập tất cả lỗi trường này bằng cách {{ form.username.errors }}. Để truy cập từng lỗi bạn thực hiện for:
{% for error in form.username.errors %}
{{ error|escape }}   
{% endfor %}
  • Lỗi không xác định: Là lỗi nào đó mà không phải chỉ phụ thuộc của mỗi riêng trường nào. Ví dụ nếu bạn nhập tên đăng nhập và mật khẩu sai sẽ không phải là lỗi của mỗi trường username mà có thể là do trường password. Để truy cập lỗi bạn thực hiện for:
{% for error in form.non_field_errors %}
{{ error|escape }}
{% endfor %}
  • Tất cả lỗi : Bao gồm cả 2 lỗi trên. Bạn có thể truy cập tất cả lỗi bằng cách {{ form.errors }}. Thực hiện với vòng for.
{% for error in form.errors %}
{{ error|escape }}
{% endfor %}

Và đây là cách mình thực hiện hiện thị lỗi form trong Form Login:

templates/registration/login.html

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %}
Đăng nhập
{% endblock title %}
{% block main %}
<div class="text-center pt-5">
    <h1 class="display-4">Đăng nhập</h1>
    <p class="lead text-muted">Nhập thông tin phía dưới để tiến hành đăng nhập.</p>
</div>
<form action="{% url 'login' %}" method="post" style="max-width: 420px; margin: auto;">
    {% csrf_token %}

    {% for error in form.non_field_errors %}
    <div class="alert alert-danger" role="alert">
        {{ error|escape }}
    </div>
    
    {% endfor %}
    <div class="form-group">
        <label for="">Username</label>
        {% render_field form.username class="form-control" placeholder="username" %}
        {% for error in form.username.errors %}
        <div class="invalid-feedback" style="display: block">{{ error|escape }}</div>
        {% endfor %}
    </div>
    <div class="form-group">
        <label for="">Password</label>
        {% render_field form.password class="form-control" placeholder="password" %}
        {% for error in form.password.errors %}
        <div class="invalid-feedback" style="display: block">{{ error|escape }}</div>
        {% endfor %}
    </div>
    <button class="btn btn-lg btn-primary" type="submit">Đăng nhập</button>
    <p class="mt-3">
        Nếu chưa có tài khoản <a href="{% url 'signup' %}">đăng ký</a>
    </p>
</form>
{% endblock main %}

Thực hiện đăng nhập sai tại 127.0.0.1:8000/users/login/ bạn sẽ thấy:

Kết luận

Ngoài các chức năng như trên thì còn thiều chức năng reset password. Phần này sẽ được nói thêm trong bài viết khác ở phần gửi E-mail trong Django.

Các fields trong Django Auth Model: Fields

Django Auth: Github

Tất cả code của bài viết xem tại Github