อัปเดตล่าสุด Feb. 20, 2023
Login, Logout และ Sign up ล้วนเป็นสิ่งที่ทุกคนคงคุ้นกันดีอยู่แล้ว จากการใช้งานระบบเว็บหรือแอพต่าง ๆ ในชีวิตประจำวัน ที่มีการจำกัดการเข้าถึงระบบของผู้ใช้ โดยระบบเหล่านี้จะถูกรวมในหัวข้อ Authentication System ของ Django ซึ่งเมื่อเราเรียนรู้หลักการพื้นฐานต่าง ๆ เหล่านี้ในบทความนี้เสร็จแล้ว ยังสามารถนำองค์ความรู้เหล่านี้ไปใช้ต่อยอดเป็นหลักการหรือแนวคิดในเฟรมเวิร์คหรือภาษาอื่นได้เช่นกันครับ
Authentication System เป็นการตรวจสอบผู้ที่จะเข้าใช้งานระบบหรือกำหนด permissions ไม่ว่าจะเป็นการเข้าถึงแอปพลิเคชัน, การเข้าถึง network, devices อะไรต่าง ๆ โดยทั่วไปจะทำการตรวจสอบจาก credentials ของผู้ใช้ ซึ่งในบริบทนี้คือ username และ password ซึ่งหลาย ๆ คนก็น่าจะคุ้นเคยกันเป็นอย่างดีกับ 2 คำนี้ และในชีวิตประจำวันเราก็คุ้นเคยกันดีกับ authentication system ผ่านแอปพลิเคชันที่เราใช้ในชีวิตประจำวัน เช่น Facebook, YouTube และเว็บไซต์อื่น ๆ ที่ต้องมีการกำหนดสิทธิในการเข้าใช้งานของผู้ใช้
ซึ่ง Django นั้นก็มี authentication system มาให้เพียบพร้อมใช้งาน
User objects เรียกได้ว่าเป็นหัวใจหลักของระบบ Authentication System ของ Django เลยก็ว่าได้ ซึ่ง attributes ซึ่งเป็นตัว default ของ User มีดังต่อไปนี้
มีมาให้พร้อมกับ 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',
...
]
เมื่อสร้างโปรเจคท์ขึ้นมาเรียบร้อยแล้ว สามารถทำการ migrate ได้ทันที ซึ่งตารางที่เก็บข้อมูลของ user จะมีชื่อว่า "auth_user" ซึ่งก็จะเก็บข้อมูลซึ่งจะมีฟีลด์ต่าง ๆ ดังต่อไปนี้
ซึ่งฟีลด์ทั้งหมดที่กล่าวมาด้านบน มี 2 ฟีลด์ที่เป็น required fields คือ username และ password คือจะปล่อยเป็นค่าว่างไม่ได้ ต้องใส่ข้อมูลเข้าไป ดังจะเห็นได้ในตอนสร้าง superuser ซึ่งจำเป็นต้องกรอก 2 ฟีลด์นี้ แต่ตัวอื่น ๆ เช่น email, etc ปล่อยเป็น blank (ว่าง) ได้
เมื่อทำการ migrate ตารางต่าง ๆ รวมไปถึง auth_user ด้านบนก็จะถูกสร้างและเก็บใน database
$ python manage.py migrate
ปกติแล้วหลาย ๆ คนคงคุ้นเคยกับการสร้าง 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 เสร็จแล้ว จากนั้นก็ทำการดึงข้อมูลมาแสดงผลเพื่อดูว่ามีกี่ 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 ที่เพิ่งสร้างไปเมื่อสักครู่
ทดสอบแสดงผล 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() ซึ่งในฟังก์ชันนี้จะทำการรับค่าเป็น 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 กันครับซึ่งจะแบ่งออกเป็น 4 ส่วนดังต่อไปนี้ คือ Login, Logout, Sign up และ Permission
การทำระบบ Login
การ login จะมีโปรเซสดังต่อไปนี้
สร้างฟังก์ชัน 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() ที่ 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)
...
]
เราได้เว็บ ๆ หนึ่งมาแล้ว เรามีหน้า ๆ หนึ่งที่ไม่ต้องการเปิด 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 ดังภาพด้านล่าง
เมื่อสร้างฟีเจอร์ 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 ที่จะพาคุณอัพสกิลและพัฒนาสู่การเป็นมืออาชีพ เรียนออนไลน์ เรียนจากที่ไหนก็ได้ พร้อมซัพพอร์ตหลังเรียน
เรียนเขียนโปรแกรม