Django Login Logout & Sign up ครบ จบในบทความเดียว

   By: Withoutcoffee Icantbedev

   อัปเดตล่าสุด Feb. 20, 2023

Django Login Logout & Sign up ครบ จบในบทความเดียว

Login, Logout และ Sign up ล้วนเป็นสิ่งที่ทุกคนคงคุ้นกันดีอยู่แล้ว จากการใช้งานระบบเว็บหรือแอพต่าง ๆ ในชีวิตประจำวัน ที่มีการจำกัดการเข้าถึงระบบของผู้ใช้ โดยระบบเหล่านี้จะถูกรวมในหัวข้อ Authentication System ของ Django ซึ่งเมื่อเราเรียนรู้หลักการพื้นฐานต่าง ๆ เหล่านี้ในบทความนี้เสร็จแล้ว ยังสามารถนำองค์ความรู้เหล่านี้ไปใช้ต่อยอดเป็นหลักการหรือแนวคิดในเฟรมเวิร์คหรือภาษาอื่นได้เช่นกันครับ

Prerequisite


Authentication System คืออะไร ?

Authentication System เป็นการตรวจสอบผู้ที่จะเข้าใช้งานระบบหรือกำหนด permissions ไม่ว่าจะเป็นการเข้าถึงแอปพลิเคชัน, การเข้าถึง network, devices อะไรต่าง ๆ โดยทั่วไปจะทำการตรวจสอบจาก credentials ของผู้ใช้ ซึ่งในบริบทนี้คือ username และ password ซึ่งหลาย ๆ คนก็น่าจะคุ้นเคยกันเป็นอย่างดีกับ 2 คำนี้  และในชีวิตประจำวันเราก็คุ้นเคยกันดีกับ authentication system ผ่านแอปพลิเคชันที่เราใช้ในชีวิตประจำวัน เช่น Facebook, YouTube และเว็บไซต์อื่น ๆ ที่ต้องมีการกำหนดสิทธิในการเข้าใช้งานของผู้ใช้

ซึ่ง Django นั้นก็มี authentication system มาให้เพียบพร้อมใช้งาน


Django's built in authentication (User objects)

User objects เรียกได้ว่าเป็นหัวใจหลักของระบบ Authentication System ของ Django เลยก็ว่าได้ ซึ่ง attributes ซึ่งเป็นตัว default ของ User มีดังต่อไปนี้

  •  username 
  •  password 
  •  first_name 
  •  last_name 
  •  email 


Default Authentication and Session Apps

มีมาให้พร้อมกับ Django เรียบร้อย เมื่อเปิดเข้าดูใน  settings.py   จะพบกับ  INSTALLED_APPS[...]   ที่มี  auth  และ   contenttypes   และใน   MIDDLEWARE[...]  ก็จะมีทั้ง  SessionMiddleware  และ 

  AuthenticationMiddleware  

settings.py

# settings.py
INSTALLED_APPS = [
    ...
    'django.contrib.auth',
    'django.contrib.contenttypes',
    ...
]

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
]


User Table (auth_user)

เมื่อสร้างโปรเจคท์ขึ้นมาเรียบร้อยแล้ว สามารถทำการ migrate ได้ทันที ซึ่งตารางที่เก็บข้อมูลของ user จะมีชื่อว่า "auth_user" ซึ่งก็จะเก็บข้อมูลซึ่งจะมีฟีลด์ต่าง ๆ ดังต่อไปนี้

  • id
  • username (required)
  • password (required)
  • email
  • is_superuser
  • is_staff
  • last_login
  • is_active
  • first_name
  • last_name
  • date_joined


ซึ่งฟีลด์ทั้งหมดที่กล่าวมาด้านบน มี 2 ฟีลด์ที่เป็น required fields คือ  username  และ  password  คือจะปล่อยเป็นค่าว่างไม่ได้ ต้องใส่ข้อมูลเข้าไป ดังจะเห็นได้ในตอนสร้าง superuser ซึ่งจำเป็นต้องกรอก 2 ฟีลด์นี้ แต่ตัวอื่น ๆ เช่น email, etc ปล่อยเป็น blank (ว่าง) ได้


เมื่อทำการ  migrate ตารางต่าง ๆ รวมไปถึง  auth_user  ด้านบนก็จะถูกสร้างและเก็บใน database

$ python manage.py migrate


การสร้าง superuser

ปกติแล้วหลาย ๆ คนคงคุ้นเคยกับการสร้าง username และ password ผ่านคำสั่ง

$ python manage.py createsuperuser

ซึ่งจริง ๆ แล้วนอกจากสร้างผ่าน  createsuperuser  เรายังสามารถสร้าง user เพื่อเข้าใช้งาน สามารถทำได้ด้วยวิธีที่ง่ายและตรงไปตรงมาที่สุดด้วยการเรียกใช้งาน helper function ที่มีชื่อว่า  create_user() 


โดยต้องทำการเข้าใช้งานหน้า Django  Shell  ก่อน

$ python manage.py shell


terminal/shell

# Shell
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user('Sonny', 'sonnypassword')

# Save this user to a database
>>> user.save()



เช็คว่ามี user ไหนบ้างใน database

หลังจากที่ได้ทำการทดสอบสร้าง user เสร็จแล้ว จากนั้นก็ทำการดึงข้อมูลมาแสดงผลเพื่อดูว่ามีกี่ users ใน database

terminal/shell

# Shell
>>> from django.contrib.auth.models import User

# Get all users from the database
>>> user = User.objects.all()
>>> user
<QuerySet [<User: Sonny>]>

จะเห็นว่ามีเพียงแค่ user ที่มีชื่อว่า  Sonny  ที่เพิ่งสร้างไปเมื่อสักครู่


Django Password Hash

ทดสอบแสดงผล password ที่ได้สร้างก่อนหน้า จะเห็นว่า Django นั้นได้ทำการ hash password ไว้เป็น default ของ Django อยู่แล้ว ดังนั้นจึงไม่ต้องเขียนเพื่อทำ hash เอง และแน่นอนว่า password ที่แสดงก็จะไม่แสดงแบบ raw text หรือ plaintext แต่จะแสดงเป็น password ที่ถูกเข้ารหัสนั่นเอง ทำให้เป็นไปได้ยากที่คนอื่นจะรู้รหัสผ่านของเรา

ซึ่ง Django ฟังก์ชันเข้ารหัสแบบ PBKDF2 (Password-Based Key Derivation Function 2) ซึ่งมีความปลอดภัยค่อนข้างสูงมากในปัจจุบัน

<algorithm>$<iterations>$<salt>$<hash>


ทดสอบสร้างรหัสผ่าน Django Shell

$ python manage.py shell

จากนั้น

# Shell
>>> from django.contrib.auth.models import User

>>> u = User.objects.get(id=1)
>>> u.username
'sonny'
>>> u.password
# hashed password 
'pbkdf2_sha256$216000$eDtCSPqq7alM$oqF2Gepvqba23c23GwtcTwrmUhhW+gEXKmwNdVg3lLs='

จากรหัสผ่านแบบ plain text หรือ raw password ที่ยังไม่ได้ถูก hash

# plantex/raw password 
'admin1234'

ถ้าเกิดกรณีที่คนเข้าถึงฐานข้อมูลเราได้หรือกรณีรั่วไหลของฐานข้อมูล เรียกได้ว่า game over เลย

แต่ Django มีระบบความปลอดภัยในการแปลง plaintext ไปเป็นอักขระยาวเหยียด (อธิบายแบบดิบ ๆ) ตรงนี้แหละคือ password ที่ถูก hash เรียบร้อย

# hashed password 
'pbkdf2_sha256$216000$eDtCSPqq7alM$oqF2Gepvqba23c23GwtcTwrmUhhW+gEXKmwNdVg3lLs='

แม้คนที่ได้รหัสผ่านตัวนี้ของเราไป ในเบื้องต้นก็คงปวดหัวและก็จะไม่รู้ว่ารหัสผ่านจริง ๆ ของเราคืออะไรกันแน่

และอีกกรณีถ้าเป็นคนที่มีความรู้ด้านโปรแกรมมิ่งหน่อย อยากแปลงกลับไปเป็นรหัส plaintext จากต้นทางแบบเดิมก็ไม่สามารถทำได้ (ไม่สามารถ decrypt แปลงกลับไปเป็นรหัสต้นทางได้อีก) เพราะถูก hash ไว้เรียบร้อย


Authenticate User

ลองทดสอบการ authenticate user ได้โดยฟังก์ชัน  authenticate()    ซึ่งในฟังก์ชันนี้จะทำการรับค่าเป็น keyword arguments 2 ตัวคือ  username   และ  password  ดังต่อไปนี้ (ซึ่ง 2 ตัวนี้แหละที่เราจะรับ input เข้ามาจากหน้าที่ user กรอกฟอร์ม มีเพิ่มเติมใน workshop section ด้านล่าง)

terminal/shell

# views.py
from django.contrib.auth import authenticate
user = authenticate(username='Sonny', password='secret-password')
if user is not None:
    # A backend authenticated the credentials
    # do_something
else:
    # No backend authenticated the credentials
    # do_something


views.py

# views.py
from django.db import models

@login_required(login_url='/login-form')
def protected_page(request):
    return render(request, 'blog/protected-page.html')


จากโค้ดด้านบนในส่วนของ decorator คือ  @login_required()  จะมีการส่งอากิวเมนต์ที่มีชื่อว่า  login_url  ซึ่งในอากิวเมนต์นี้สามารถส่ง url เข้าไปได้เลย โดยถ้า user กดเข้าดูหน้านี้ก็จะไม่สามารถเข้าดูได้ถ้ายังไม่ได้ทำการ authenticate และจะถูกลิ้งค์ไปที่หน้า /login-form ที่ถูกสร้างในฟังก์ชัน   login_form()  ให้โดยอัตโนมัติ โดยในฟังก์ชันนี้ก็ทำการ render หน้า login เพื่อให้ user สามารถล็อกอินเข้าใช้ได้นั่นเอง



Workshop

เราจะมาทำ workshop กันครับซึ่งจะแบ่งออกเป็น 4 ส่วนดังต่อไปนี้ คือ Login, Logout, Sign up และ Permission


การทำระบบ Login

การ login จะมีโปรเซสดังต่อไปนี้

  • User ทำการล็อกอินเข้าใช้งานผ่านหน้า login ฟอร์ม แล้วกด submit
  • Server ทำการเช็ค credentials(username & password) ของ user ที่ได้กรอกเข้ามา
  • ถ้า username และ password ตรงกับข้อมูลในฐานข้อมูลที่ได้ลงทะเบียนไว้ก่อนหน้า ผู้ใช้งานสามารถเข้าใช้งานในระบบได้
  • ถ้า username และ password ไม่ตรง ให้แสดงข้อความอะไรบางอย่าง เช่น "Incorrect username or password" เป็นต้น (มีสอนเพิ่มเติมในหัวข้อ Django Messages Framework)


สร้างฟังก์ชัน  login_user()   เพื่อที่จะกำหนดเงื่อนไขต่าง ๆ ในการล็อกอินเข้าใช้งานและตรวจสอบ username และ password ที่ user ได้ทำการกรอกเข้ามาในหน้า login form

views.py

# blog/views.py
...
from django.contrib.auth import login  # Import login function
...

def sign_in(request):
    if request.method == "POST":
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = authenticate(
            request, 
            username=username, 
            password=password
        )

        if user is not None:
            # Log user in
            login(request, user)
            return redirect('/')
            
    return render(request, 'blog/sign-in.html')
...


home.html

<!-- home.html -->
{% extends 'blog/base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
<div class="container"><br>
    <h1>Hello, Django MU</h1>
    <p>I love to learn Django framework</p>
    <div class="row">
        {% for post in all_posts %}
        <div class="col-sm-4">
          <div class="card">
            <div class="card-body">
              <img src="{{ post.feature_image.url }}" alt="{{ post.title }}" class="img-fluid"><br><br>
              <h5 class="card-title">{{ post.title }}</h5>
              <p class="card-text text-secondary">{{ post.short_description }}</p>
              <p class="card-text text-secondary">Last update: {{ post.date_updated | date }}</p>
              <a href="{% url 'post-detail' post.id %}" class="btn btn-primary">Read more</a>
            </div>
          </div>
        </div>
        {% endfor %}
    </div>
  </div>
{% endblock %}




ทำการสร้างหน้า UI (Form) ที่มีชื่อว่า  sign-in.html  เพื่อให้ user สามารถล็อกอินเข้ามาใช้งานได้

sign-in.html

<!-- sign-in.html -->
{% extends "blog/base.html" %}
{% block title %}Sign in{% endblock %}
{% block content %}
<div class="container">
  <h1>Sign in</h1>
  <form method="POST" action="{% url 'sign-in' %}">
      {% csrf_token %}  
      <div class="col-md-4">
          <div class="form-group">
            <label>Username</label>
            <input type="text" class="form-control" name="username">
          </div>
          <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" name="password">
          </div>
          <button type="submit" class="btn btn-primary">Sign in</button>
      </div>
  </form>
</div>
{% endblock %}


หน้า Login


การ Logout

การ logout ก็สามารถทำได้สะดวกและง่ายมาก ๆ ด้วยฟังก์ชันที่สำเร็จรูปมาให้แล้วอย่าง logout  logout()  ที่ Django ทำมาให้สำเร็จเรียบร้อย ซึ่งในฟังก์ชันนี้ต้องการ 1 argument นั่นก็คือ  request  จากนั้นก็ทำการส่งอากิวเมนต์นี้เข้าไปให้เรียบร้อยร้อย จะได้  logout(request)  

จากนั้นทำการรีเทิร์นข้อความที่มีชื่อว่า  You've logged our. Get back to log in  "ok"


views.py

# blog/views.py
...
from django.contrib.auth import login, logout  # Import logout function
...

def sign_out(request):
    # sign user out
    logout(request)

    # Redirect to sign-in page
    return redirect('/sign-in')
...




log out สำเร็จ


urls.py

from django.urls import path 
from .views import (
    home, about, post_detail, contact,
    search, sign_up, sign_in, sign_out
  )

urlpatterns = [
    ...
    path('', home),
    path("posts/<int:post_id>", post_detail, name="post-detail"),
    path('sign-up', sign_up),
    path('sign-in', sign_in),
    path('sign-out', sign_out)
    ...
]


User Permission

เราได้เว็บ ๆ หนึ่งมาแล้ว เรามีหน้า ๆ หนึ่งที่ไม่ต้องการเปิด publicให้ทุกคนสามารถสามารถเข้าถึงได้ จะเป็น user ที่มี permission แล้วเท่านั้นถึงจะเข้าดูหน้าเว็บหน้านั้นได้ คือ user ที่ได้ login แล้วนั่นเอง

ซึ่งแน่นอนว่า Django ก็มีระบบนี้มาให้เรียบร้อยครับในโมดูล decorators ของ Django

# blog/views.py
from django.contrib.auth.decorators import login_required


การใช้งานก็ทำได้โดยนำฟังก์ชัน decorator   @login_required()   ไปวางไว้บนสุดของฟังก์ชันหรือหน้าเพจที่เราต้องการทำ permissionโดยมีการใส่อากิวเมนต์เข้าไปเพิ่มคือ    login_url="/sign-in"  เพื่อให้ redirect ไปที่หน้า login ถ้า user อยากเข้าชมหน้านี้ต้อง login ก่อนนะ

@login_required(login_url="/sign-in")


post-detail.html

# blog/views.py
...
from django.contrib.auth.decorators import login_required
...

@login_required(login_url="/sign-in") # Protect this page, and redirect to sign-in page
def post_detail(request, post_id):
    post = Post.objects.get(id=post_id)
    return render(request, 'blog/post-detail.html', {'post': post})
...


เมื่อลองคลิก Read more ในแต่ละโพสต์จะปรากฎหน้า Page not found ดังภาพด้านล่าง


ซึ่งนี่ก็ถือว่าเข้าเงื่อนไขของ permission แล้วครับ เพราะต้องเป็น user ที่ได้ login เข้ามาแล้วเท่านั่นถึงจะเข้าหน้านี้ได้



การทำระบบ Sign up

เมื่อสร้างฟีเจอร์ login และ logout และทำ user permission เสร็จแล้ว พร้อมใช้งานได้เรียบร้อยไม่มีปัญหา ต่อมาก็จะเป็นการสร้างหน้าลงทะเบียนหรือ sign up page เพราะว่าตอนนี้เรายังไม่ได้มีระบบนี้ครับ

forms.py

# blog/forms.py
from django import forms 
from .models import Contact
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User 
...

class RegisterForm(UserCreationForm):
    email = forms.EmailField()
    model = User
    fields = [
        "username",
        "email",
        "password1",
        "password2",
    ]


views.py

# blog/views.py
from django.shortcuts import render, redirect
from .forms import ContactForm, RegisterForm
...
def sign_up(request):
    if request.method == "POST":
        form = RegisterForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('/')
    else:
        form = RegisterForm()
        print(form)

    return render(request, 'blog/sign-up.html', {'form': form})
...


sign-up.html

<!-- sign-up.html -->
{% extends 'blog/base.html' %}
{% load crispy_forms_tags %}
{% block title %}Sign up{% endblock %}
{% block content %}
<div class="container"><br>
    <div class="col-lg-5">
        <h1>Sign up</h1>
        <form method="POST" action="{% url 'sign-up' %}">
            {% csrf_token %}  
            {{ form | crispy }}
            <button type="submit" class="btn btn-primary">Sign up</button>
        </form><br>
    </div>
</div>
{% endblock content %}




สรุป

จบลงไปแล้วกับบทความ Django log in, log out และ sign up หวังว่าหลังจากอ่านจนจบ ทุกคนจะเข้าใจโปรเซสและภาพรวมของระบบ authentication system ด้วย django framework กันนะครับ 


บทความแนะนำ


References 

[ djangoproject.com ] - Using the Djagno authentication system

[developer.mozilla.org ] - User Authentication and Permissions


เปิดโลกการเขียนโปรแกรมและ Software Development ด้วย online courses ที่จะพาคุณอัพสกิลและพัฒนาสู่การเป็นมืออาชีพ เรียนออนไลน์ เรียนจากที่ไหนก็ได้ พร้อมซัพพอร์ตหลังเรียน

คอร์สเรียนเขียนโปรแกรม